Skip to content

Commit b01eb98

Browse files
annikahannigrjarry
authored andcommitted
data: add diff support
This patch adds support for the `lyd_diff` function. Signed-off-by: Annika Hannig <annika@hannig.cc>
1 parent edef470 commit b01eb98

File tree

4 files changed

+183
-2
lines changed

4 files changed

+183
-2
lines changed

cffi/cdefs.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ typedef enum {
410410
#define LYD_OPT_DESTRUCT ...
411411
#define LYD_OPT_NOSIBLINGS ...
412412

413+
#define LYD_DIFFOPT_NOSIBLINGS ...
414+
#define LYD_DIFFOPT_WITHDEFAULTS ...
415+
413416
typedef union lyd_value_u {
414417
int8_t bln;
415418
struct lyd_node *leafref;
@@ -434,6 +437,23 @@ struct lyd_node_leaf_list {
434437
...;
435438
};
436439

440+
typedef enum {
441+
LYD_DIFF_END,
442+
LYD_DIFF_DELETED,
443+
LYD_DIFF_CHANGED,
444+
LYD_DIFF_MOVEDAFTER1,
445+
LYD_DIFF_CREATED,
446+
LYD_DIFF_MOVEDAFTER2,
447+
...
448+
} LYD_DIFFTYPE;
449+
450+
struct lyd_difflist {
451+
LYD_DIFFTYPE *type;
452+
struct lyd_node **first;
453+
struct lyd_node **second;
454+
...;
455+
};
456+
437457
struct ly_set *lyd_find_instance(const struct lyd_node *, const struct lys_node *);
438458
struct ly_set *lyd_find_path(const struct lyd_node *, const char *);
439459
struct lyd_node *lyd_new(struct lyd_node *, const struct lys_module *, const char *);
@@ -454,6 +474,8 @@ void lyd_free(struct lyd_node *);
454474
void lyd_free_withsiblings(struct lyd_node *);
455475
int lyd_validate(struct lyd_node **, int, void *);
456476
int lyd_merge(struct lyd_node *, const struct lyd_node *, int);
477+
struct lyd_difflist *lyd_diff(struct lyd_node *first, struct lyd_node *second, int options);
478+
void lyd_free_diff(struct lyd_difflist *diff);
457479

458480
/* from libc, needed to free allocated strings */
459481
void free(void *);

libyang/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
from .context import Context
7-
from .data import DContainer, DLeaf, DLeafList, DList, DNode, DRpc
7+
from .data import DContainer, DDiff, DLeaf, DLeafList, DList, DNode, DRpc
88
from .diff import (
99
BaseTypeAdded,
1010
BaseTypeRemoved,
@@ -90,6 +90,7 @@
9090
"ConfigFalseRemoved",
9191
"Context",
9292
"DContainer",
93+
"DDiff",
9394
"DLeaf",
9495
"DLeafList",
9596
"DList",

libyang/data.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,63 @@ def parser_flags(
110110
return flags
111111

112112

113+
def diff_flags(
114+
with_defaults: bool = False,
115+
no_siblings: bool = False,
116+
) -> int:
117+
flags = 0
118+
if with_defaults:
119+
flags |= lib.LYD_DIFFOPT_WITHDEFAULTS
120+
if no_siblings:
121+
flags |= lib.LYD_DIFFOPT_NOSIBLINGS
122+
return flags
123+
124+
125+
# -------------------------------------------------------------------------------------
126+
class DDiff:
127+
"""
128+
Data tree diff
129+
"""
130+
131+
DELETED = lib.LYD_DIFF_DELETED
132+
CHANGED = lib.LYD_DIFF_CHANGED
133+
CREATED = lib.LYD_DIFF_CREATED
134+
MOVEDAFTER1 = lib.LYD_DIFF_MOVEDAFTER1
135+
MOVEDAFTER2 = lib.LYD_DIFF_MOVEDAFTER2
136+
137+
DIFF_TYPES = {
138+
DELETED: "deleted",
139+
CHANGED: "changed",
140+
CREATED: "created",
141+
MOVEDAFTER1: "movedafter1",
142+
MOVEDAFTER2: "movedafter2",
143+
}
144+
145+
__slots__ = ("dtype", "first", "second")
146+
147+
def __init__(self, dtype, first: Optional["DNode"], second: Optional["DNode"]):
148+
"""
149+
:arg dtype:
150+
The type of the diff
151+
:arg first:
152+
The first DNode
153+
:arg second:
154+
The second DNode
155+
"""
156+
self.dtype = dtype
157+
self.first = first
158+
self.second = second
159+
160+
def diff_type(self) -> str:
161+
"""Get diff type as string"""
162+
return self.DIFF_TYPES.get(self.dtype, "unknown")
163+
164+
def __repr__(self) -> str:
165+
return "<libyang.data.DDiff {} first={} second={}>".format(
166+
self.diff_type(), self.first, self.second
167+
)
168+
169+
113170
# -------------------------------------------------------------------------------------
114171
class DNode:
115172
"""
@@ -224,6 +281,31 @@ def validate(
224281
if ret != 0:
225282
raise self.context.error("validation failed")
226283

284+
def diff(
285+
self,
286+
other: "DNode",
287+
no_siblings: bool = False,
288+
with_defaults: bool = False,
289+
) -> Iterator[DDiff]:
290+
flags = diff_flags(no_siblings=no_siblings, with_defaults=with_defaults)
291+
dlist = lib.lyd_diff(self.cdata, other.cdata, flags)
292+
if not dlist:
293+
raise self.context.error("diff failed")
294+
295+
i = 0
296+
try:
297+
while dlist.type[i] != lib.LYD_DIFF_END:
298+
first = None
299+
if dlist.first[i]:
300+
first = self.new(self.context, dlist.first[i])
301+
second = None
302+
if dlist.second[i]:
303+
second = self.new(self.context, dlist.second[i])
304+
yield DDiff(dlist.type[i], first, second)
305+
i += 1
306+
finally:
307+
lib.lyd_free_diff(dlist)
308+
227309
def merge(
228310
self,
229311
source: "DNode",

tests/test_data.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from unittest.mock import patch
88

99
from _libyang import lib
10-
from libyang import Context, DContainer, DRpc, LibyangError
10+
from libyang import Context, DContainer, DDiff, DRpc, LibyangError
1111

1212

1313
YANG_DIR = os.path.join(os.path.dirname(__file__), "yang")
@@ -489,3 +489,79 @@ def test_data_to_dict_action(self):
489489
},
490490
},
491491
)
492+
493+
XML_DIFF_STATE1 = """<state xmlns="urn:yang:yolo:system">
494+
<hostname>foo</hostname>
495+
<speed>1234</speed>
496+
<number>1000</number>
497+
<number>2000</number>
498+
<number>3000</number>
499+
<url>
500+
<proto>https</proto>
501+
<host>github.com</host>
502+
<path>/CESNET/libyang-python</path>
503+
<enabled>false</enabled>
504+
</url>
505+
<url>
506+
<proto>http</proto>
507+
<host>foobar.com</host>
508+
<port>8080</port>
509+
<path>/index.html</path>
510+
<enabled>true</enabled>
511+
</url>
512+
</state>
513+
"""
514+
XML_DIFF_STATE2 = """<state xmlns="urn:yang:yolo:system">
515+
<hostname>foo</hostname>
516+
<speed>5432</speed>
517+
<number>1000</number>
518+
<number>3000</number>
519+
<url>
520+
<proto>https</proto>
521+
<host>github.com</host>
522+
<path>/CESNET/libyang-python</path>
523+
<enabled>true</enabled>
524+
</url>
525+
<url>
526+
<proto>http</proto>
527+
<host>foobar.com</host>
528+
<port>8080</port>
529+
<path>/index.html</path>
530+
<enabled>false</enabled>
531+
</url>
532+
<url>
533+
<proto>ftp</proto>
534+
<host>github.com</host>
535+
<path>/CESNET/libyang-python</path>
536+
<enabled>false</enabled>
537+
</url>
538+
</state>
539+
"""
540+
541+
def test_data_diff(self):
542+
dnode1 = self.ctx.parse_data_mem(
543+
self.XML_DIFF_STATE1, "xml", data=True, no_yanglib=True
544+
)
545+
self.assertIsInstance(dnode1, DContainer)
546+
dnode2 = self.ctx.parse_data_mem(
547+
self.XML_DIFF_STATE2, "xml", data=True, no_yanglib=True
548+
)
549+
self.assertIsInstance(dnode2, DContainer)
550+
551+
diffs = dnode1.diff(dnode2)
552+
diffs_result = [
553+
(diff.dtype, diff.first.name(), diff.second.name() if diff.second else None)
554+
for diff in diffs
555+
]
556+
expected = [
557+
(DDiff.CHANGED, "speed", "speed"),
558+
(DDiff.CHANGED, "enabled", "enabled"),
559+
(DDiff.CHANGED, "enabled", "enabled"),
560+
(DDiff.DELETED, "number", None),
561+
(DDiff.CREATED, "state", "url"),
562+
]
563+
564+
self.assertListEqual(diffs_result, expected)
565+
566+
dnode1.free()
567+
dnode2.free()

0 commit comments

Comments
 (0)