Skip to content

Commit ce82742

Browse files
committed
libyang3-py3: patch for backlinks support
CESNET/libyang-python#132
1 parent fee2224 commit ce82742

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
From ebb79c5fec9635b5efb7a24daf1537ba91491f6a Mon Sep 17 00:00:00 2001
2+
From: Brad House <brad@brad-house.com>
3+
Date: Sun, 16 Feb 2025 11:04:50 -0500
4+
Subject: [PATCH] schema/context: restore some backlinks support
5+
6+
In libyang v1 the schema nodes had a backlinks member to be able to
7+
look up dependents of the node. SONiC depends on this to provide
8+
functionality it uses and it needs to be exposed via the python
9+
module.
10+
11+
In theory, exposing the 'dfs' functions could make this work, but
12+
it would likely be cost prohibitive since walking the tree would
13+
be expensive to create a python node for evaluation in native
14+
python.
15+
16+
Instead this PR depends on the this libyang PR:
17+
https://github.com/CESNET/libyang/pull/2352
18+
And adds thin wrappers.
19+
20+
This implementation provides 2 python functions:
21+
* Context.find_backlinks_paths() - This function can
22+
take the path of the base node and find all dependents. If
23+
no path is specified, then it will return all nodes that contain
24+
a leafref reference.
25+
* Context.find_leafref_path_target_paths() - This function takes
26+
an xpath, then returns all target nodes the xpath may reference.
27+
Typically only one will be returned, but multiples may be in the
28+
case of a union.
29+
30+
A user can build a cache by combining Context.find_backlinks_paths()
31+
with no path set and building a reverse table using
32+
Context.find_leafref_path_target_paths()
33+
34+
Signed-off-by: Brad House <brad@brad-house.com>
35+
---
36+
cffi/cdefs.h | 2 +
37+
libyang/context.py | 98 +++++++++++++++++++
38+
tests/test_schema.py | 58 +++++++++++
39+
.../yang/yolo/yolo-leafref-search-extmod.yang | 39 ++++++++
40+
tests/yang/yolo/yolo-leafref-search.yang | 36 +++++++
41+
5 files changed, 233 insertions(+)
42+
create mode 100644 tests/yang/yolo/yolo-leafref-search-extmod.yang
43+
create mode 100644 tests/yang/yolo/yolo-leafref-search.yang
44+
45+
diff --git a/cffi/cdefs.h b/cffi/cdefs.h
46+
index aa75004..e7dc978 100644
47+
--- a/cffi/cdefs.h
48+
+++ b/cffi/cdefs.h
49+
@@ -861,6 +861,8 @@ const struct lysc_node* lys_find_child(const struct lysc_node *, const struct ly
50+
const struct lysc_node* lysc_node_child(const struct lysc_node *);
51+
const struct lysc_node_action* lysc_node_actions(const struct lysc_node *);
52+
const struct lysc_node_notif* lysc_node_notifs(const struct lysc_node *);
53+
+LY_ERR lysc_node_lref_targets(const struct lysc_node *, struct ly_set **);
54+
+LY_ERR lysc_node_lref_backlinks(const struct ly_ctx *, const struct lysc_node *, ly_bool, struct ly_set **);
55+
56+
typedef enum {
57+
LYD_PATH_STD,
58+
diff --git a/libyang/context.py b/libyang/context.py
59+
index fb4a330..d6eb7a3 100644
60+
--- a/libyang/context.py
61+
+++ b/libyang/context.py
62+
@@ -646,6 +646,104 @@ def parse_data_file(
63+
json_null=json_null,
64+
)
65+
66+
+ def find_leafref_path_target_paths(self, leafref_path: str) -> list[str]:
67+
+ """
68+
+ Fetch all leafref targets of the specified path
69+
+
70+
+ This is an enhanced version of lysc_node_lref_target() which will return
71+
+ a set of leafref target paths retrieved from the specified schema path.
72+
+ While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and
73+
+ LYS_LEAFLIST this function will also evaluate other datatypes that may
74+
+ contain leafrefs such as LYS_UNION. This does not, however, search for
75+
+ children with leafref targets.
76+
+
77+
+ :arg self
78+
+ This instance on context
79+
+ :arg leafref_path:
80+
+ Path to node to search for leafref targets
81+
+ :returns List of target paths that the leafrefs of the specified node
82+
+ point to.
83+
+ """
84+
+ if self.cdata is None:
85+
+ raise RuntimeError("context already destroyed")
86+
+ if leafref_path is None:
87+
+ raise RuntimeError("leafref_path must be defined")
88+
+
89+
+ out = []
90+
+
91+
+ node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0)
92+
+ if node == ffi.NULL:
93+
+ raise self.error("leafref_path not found")
94+
+
95+
+ node_set = ffi.new("struct ly_set **")
96+
+ if (lib.lysc_node_lref_targets(node, node_set) != lib.LY_SUCCESS or
97+
+ node_set[0] == ffi.NULL or node_set[0].count == 0):
98+
+ raise self.error("leafref_path does not contain any leafref targets")
99+
+
100+
+ node_set = node_set[0]
101+
+ for i in range(node_set.count):
102+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
103+
+ out.append(c2str(path))
104+
+ lib.free(path)
105+
+
106+
+ lib.ly_set_free(node_set, ffi.NULL)
107+
+
108+
+ return out
109+
+
110+
+
111+
+ def find_backlinks_paths(self, match_path: str = None, match_ancestors: bool = False) -> list[str]:
112+
+ """
113+
+ Search entire schema for nodes that contain leafrefs and return as a
114+
+ list of schema node paths.
115+
+
116+
+ Perform a complete scan of the schema tree looking for nodes that
117+
+ contain leafref entries. When a node contains a leafref entry, and
118+
+ match_path is specified, determine if reference points to match_path,
119+
+ if so add the node's path to returned list. If no match_path is
120+
+ specified, the node containing the leafref is always added to the
121+
+ returned set. When match_ancestors is true, will evaluate if match_path
122+
+ is self or an ansestor of self.
123+
+
124+
+ This does not return the leafref targets, but the actual node that
125+
+ contains a leafref.
126+
+
127+
+ :arg self
128+
+ This instance on context
129+
+ :arg match_path:
130+
+ Target path to use for matching
131+
+ :arg match_ancestors:
132+
+ Whether match_path is a base ancestor or an exact node
133+
+ :returns List of paths. Exception of match_path is not found or if no
134+
+ backlinks are found.
135+
+ """
136+
+ if self.cdata is None:
137+
+ raise RuntimeError("context already destroyed")
138+
+ out = []
139+
+
140+
+ match_node = ffi.NULL
141+
+ if match_path is not None and match_path == "/" or match_path == "":
142+
+ match_path = None
143+
+
144+
+ if match_path:
145+
+ match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0)
146+
+ if match_node == ffi.NULL:
147+
+ raise self.error("match_path not found")
148+
+
149+
+ node_set = ffi.new("struct ly_set **")
150+
+ if (lib.lysc_node_lref_backlinks(self.cdata, match_node, match_ancestors, node_set)
151+
+ != lib.LY_SUCCESS or node_set[0] == ffi.NULL or node_set[0].count == 0):
152+
+ raise self.error("backlinks not found")
153+
+
154+
+ node_set = node_set[0]
155+
+ for i in range(node_set.count):
156+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
157+
+ out.append(c2str(path))
158+
+ lib.free(path)
159+
+
160+
+ lib.ly_set_free(node_set, ffi.NULL)
161+
+
162+
+ return out
163+
+
164+
def __iter__(self) -> Iterator[Module]:
165+
"""
166+
Return an iterator that yields all implemented modules from the context
167+
diff --git a/tests/test_schema.py b/tests/test_schema.py
168+
index a310aad..4aae73a 100644
169+
--- a/tests/test_schema.py
170+
+++ b/tests/test_schema.py
171+
@@ -801,6 +801,64 @@ def test_leaf_list_parsed(self):
172+
self.assertFalse(pnode.ordered())
173+
174+
175+
+# -------------------------------------------------------------------------------------
176+
+class BacklinksTest(unittest.TestCase):
177+
+ def setUp(self):
178+
+ self.ctx = Context(YANG_DIR)
179+
+ self.ctx.load_module("yolo-leafref-search")
180+
+ self.ctx.load_module("yolo-leafref-search-extmod")
181+
+ def tearDown(self):
182+
+ self.ctx.destroy()
183+
+ self.ctx = None
184+
+ def test_backlinks_all_nodes(self):
185+
+ expected = [
186+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
187+
+ "/yolo-leafref-search:refstr",
188+
+ "/yolo-leafref-search:refnum",
189+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
190+
+ ]
191+
+ refs = self.ctx.find_backlinks_paths()
192+
+ expected.sort()
193+
+ refs.sort()
194+
+ self.assertEqual(expected, refs)
195+
+ def test_backlinks_one(self):
196+
+ expected = [
197+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
198+
+ "/yolo-leafref-search:refstr",
199+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
200+
+ ]
201+
+ refs = self.ctx.find_backlinks_paths(
202+
+ match_path="/yolo-leafref-search:my_list/my_leaf_string"
203+
+ )
204+
+ expected.sort()
205+
+ refs.sort()
206+
+ self.assertEqual(expected, refs)
207+
+ def test_backlinks_children(self):
208+
+ expected = [
209+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
210+
+ "/yolo-leafref-search:refstr",
211+
+ "/yolo-leafref-search:refnum",
212+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
213+
+ ]
214+
+ refs = self.ctx.find_backlinks_paths(
215+
+ match_path="/yolo-leafref-search:my_list",
216+
+ match_ancestors=True
217+
+ )
218+
+ expected.sort()
219+
+ refs.sort()
220+
+ self.assertEqual(expected, refs)
221+
+ def test_backlinks_leafref_target_paths(self):
222+
+ expected = [
223+
+ "/yolo-leafref-search:my_list/my_leaf_string"
224+
+ ]
225+
+ refs = self.ctx.find_leafref_path_target_paths(
226+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref"
227+
+ )
228+
+ expected.sort()
229+
+ refs.sort()
230+
+ self.assertEqual(expected, refs)
231+
+
232+
+
233+
# -------------------------------------------------------------------------------------
234+
class ChoiceTest(unittest.TestCase):
235+
def setUp(self):
236+
diff --git a/tests/yang/yolo/yolo-leafref-search-extmod.yang b/tests/yang/yolo/yolo-leafref-search-extmod.yang
237+
new file mode 100644
238+
index 0000000..046ceec
239+
--- /dev/null
240+
+++ b/tests/yang/yolo/yolo-leafref-search-extmod.yang
241+
@@ -0,0 +1,39 @@
242+
+module yolo-leafref-search-extmod {
243+
+ yang-version 1.1;
244+
+ namespace "urn:yang:yolo:leafref-search-extmod";
245+
+ prefix leafref-search-extmod;
246+
+
247+
+ import wtf-types { prefix types; }
248+
+
249+
+ import yolo-leafref-search {
250+
+ prefix leafref-search;
251+
+ }
252+
+
253+
+ revision 2025-02-11 {
254+
+ description
255+
+ "Initial version.";
256+
+ }
257+
+
258+
+ list my_extref_list {
259+
+ key my_leaf_string;
260+
+ leaf my_leaf_string {
261+
+ type string;
262+
+ }
263+
+ leaf my_extref {
264+
+ type leafref {
265+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
266+
+ }
267+
+ }
268+
+ leaf my_extref_union {
269+
+ type union {
270+
+ type leafref {
271+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
272+
+ }
273+
+ type leafref {
274+
+ path "/leafref-search:my_list/leafref-search:my_leaf_number";
275+
+ }
276+
+ type types:number;
277+
+ }
278+
+ }
279+
+ }
280+
+}
281+
diff --git a/tests/yang/yolo/yolo-leafref-search.yang b/tests/yang/yolo/yolo-leafref-search.yang
282+
new file mode 100644
283+
index 0000000..5f4af48
284+
--- /dev/null
285+
+++ b/tests/yang/yolo/yolo-leafref-search.yang
286+
@@ -0,0 +1,36 @@
287+
+module yolo-leafref-search {
288+
+ yang-version 1.1;
289+
+ namespace "urn:yang:yolo:leafref-search";
290+
+ prefix leafref-search;
291+
+
292+
+ import wtf-types { prefix types; }
293+
+
294+
+ revision 2025-02-11 {
295+
+ description
296+
+ "Initial version.";
297+
+ }
298+
+
299+
+ list my_list {
300+
+ key my_leaf_string;
301+
+ leaf my_leaf_string {
302+
+ type string;
303+
+ }
304+
+ leaf my_leaf_number {
305+
+ description
306+
+ "A number.";
307+
+ type types:number;
308+
+ }
309+
+ }
310+
+
311+
+ leaf refstr {
312+
+ type leafref {
313+
+ path "../my_list/my_leaf_string";
314+
+ }
315+
+ }
316+
+
317+
+ leaf refnum {
318+
+ type leafref {
319+
+ path "../my_list/my_leaf_number";
320+
+ }
321+
+ }
322+
+}

src/libyang3-py3/patch/series

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
0001-debian.patch
22
0002-pr134-json-string-datatypes.patch
33
0003-parse_module-memleak.patch
4+
0004-pr132-backlinks.patch

0 commit comments

Comments
 (0)