Skip to content

Commit 2b31894

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

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
From 6f9a434420df12583256f1269aa9a722c1eecd3b Mon Sep 17 00:00:00 2001
2+
From: Brad House <[email protected]>
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 <[email protected]>
35+
---
36+
cffi/cdefs.h | 2 +
37+
libyang/context.py | 100 +++++++++++++++++-
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, 234 insertions(+), 1 deletion(-)
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..f91d1e3 100644
60+
--- a/libyang/context.py
61+
+++ b/libyang/context.py
62+
@@ -4,7 +4,7 @@
63+
# SPDX-License-Identifier: MIT
64+
65+
import os
66+
-from typing import IO, Any, Callable, Iterator, Optional, Tuple, Union
67+
+from typing import IO, Any, Callable, Iterator, Optional, Tuple, Union, List
68+
69+
from _libyang import ffi, lib
70+
from .data import (
71+
@@ -646,6 +646,104 @@ def parse_data_file(
72+
json_null=json_null,
73+
)
74+
75+
+ def find_leafref_path_target_paths(self, leafref_path: str) -> List[str]:
76+
+ """
77+
+ Fetch all leafref targets of the specified path
78+
+
79+
+ This is an enhanced version of lysc_node_lref_target() which will return
80+
+ a set of leafref target paths retrieved from the specified schema path.
81+
+ While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and
82+
+ LYS_LEAFLIST this function will also evaluate other datatypes that may
83+
+ contain leafrefs such as LYS_UNION. This does not, however, search for
84+
+ children with leafref targets.
85+
+
86+
+ :arg self
87+
+ This instance on context
88+
+ :arg leafref_path:
89+
+ Path to node to search for leafref targets
90+
+ :returns List of target paths that the leafrefs of the specified node
91+
+ point to.
92+
+ """
93+
+ if self.cdata is None:
94+
+ raise RuntimeError("context already destroyed")
95+
+ if leafref_path is None:
96+
+ raise RuntimeError("leafref_path must be defined")
97+
+
98+
+ out = []
99+
+
100+
+ node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0)
101+
+ if node == ffi.NULL:
102+
+ raise self.error("leafref_path not found")
103+
+
104+
+ node_set = ffi.new("struct ly_set **")
105+
+ if (lib.lysc_node_lref_targets(node, node_set) != lib.LY_SUCCESS or
106+
+ node_set[0] == ffi.NULL or node_set[0].count == 0):
107+
+ raise self.error("leafref_path does not contain any leafref targets")
108+
+
109+
+ node_set = node_set[0]
110+
+ for i in range(node_set.count):
111+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
112+
+ out.append(c2str(path))
113+
+ lib.free(path)
114+
+
115+
+ lib.ly_set_free(node_set, ffi.NULL)
116+
+
117+
+ return out
118+
+
119+
+
120+
+ def find_backlinks_paths(self, match_path: str = None, match_ancestors: bool = False) -> List[str]:
121+
+ """
122+
+ Search entire schema for nodes that contain leafrefs and return as a
123+
+ list of schema node paths.
124+
+
125+
+ Perform a complete scan of the schema tree looking for nodes that
126+
+ contain leafref entries. When a node contains a leafref entry, and
127+
+ match_path is specified, determine if reference points to match_path,
128+
+ if so add the node's path to returned list. If no match_path is
129+
+ specified, the node containing the leafref is always added to the
130+
+ returned set. When match_ancestors is true, will evaluate if match_path
131+
+ is self or an ansestor of self.
132+
+
133+
+ This does not return the leafref targets, but the actual node that
134+
+ contains a leafref.
135+
+
136+
+ :arg self
137+
+ This instance on context
138+
+ :arg match_path:
139+
+ Target path to use for matching
140+
+ :arg match_ancestors:
141+
+ Whether match_path is a base ancestor or an exact node
142+
+ :returns List of paths. Exception of match_path is not found or if no
143+
+ backlinks are found.
144+
+ """
145+
+ if self.cdata is None:
146+
+ raise RuntimeError("context already destroyed")
147+
+ out = []
148+
+
149+
+ match_node = ffi.NULL
150+
+ if match_path is not None and match_path == "/" or match_path == "":
151+
+ match_path = None
152+
+
153+
+ if match_path:
154+
+ match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0)
155+
+ if match_node == ffi.NULL:
156+
+ raise self.error("match_path not found")
157+
+
158+
+ node_set = ffi.new("struct ly_set **")
159+
+ if (lib.lysc_node_lref_backlinks(self.cdata, match_node, match_ancestors, node_set)
160+
+ != lib.LY_SUCCESS or node_set[0] == ffi.NULL or node_set[0].count == 0):
161+
+ raise self.error("backlinks not found")
162+
+
163+
+ node_set = node_set[0]
164+
+ for i in range(node_set.count):
165+
+ path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0);
166+
+ out.append(c2str(path))
167+
+ lib.free(path)
168+
+
169+
+ lib.ly_set_free(node_set, ffi.NULL)
170+
+
171+
+ return out
172+
+
173+
def __iter__(self) -> Iterator[Module]:
174+
"""
175+
Return an iterator that yields all implemented modules from the context
176+
diff --git a/tests/test_schema.py b/tests/test_schema.py
177+
index a310aad..4aae73a 100644
178+
--- a/tests/test_schema.py
179+
+++ b/tests/test_schema.py
180+
@@ -801,6 +801,64 @@ def test_leaf_list_parsed(self):
181+
self.assertFalse(pnode.ordered())
182+
183+
184+
+# -------------------------------------------------------------------------------------
185+
+class BacklinksTest(unittest.TestCase):
186+
+ def setUp(self):
187+
+ self.ctx = Context(YANG_DIR)
188+
+ self.ctx.load_module("yolo-leafref-search")
189+
+ self.ctx.load_module("yolo-leafref-search-extmod")
190+
+ def tearDown(self):
191+
+ self.ctx.destroy()
192+
+ self.ctx = None
193+
+ def test_backlinks_all_nodes(self):
194+
+ expected = [
195+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
196+
+ "/yolo-leafref-search:refstr",
197+
+ "/yolo-leafref-search:refnum",
198+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
199+
+ ]
200+
+ refs = self.ctx.find_backlinks_paths()
201+
+ expected.sort()
202+
+ refs.sort()
203+
+ self.assertEqual(expected, refs)
204+
+ def test_backlinks_one(self):
205+
+ expected = [
206+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
207+
+ "/yolo-leafref-search:refstr",
208+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
209+
+ ]
210+
+ refs = self.ctx.find_backlinks_paths(
211+
+ match_path="/yolo-leafref-search:my_list/my_leaf_string"
212+
+ )
213+
+ expected.sort()
214+
+ refs.sort()
215+
+ self.assertEqual(expected, refs)
216+
+ def test_backlinks_children(self):
217+
+ expected = [
218+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref",
219+
+ "/yolo-leafref-search:refstr",
220+
+ "/yolo-leafref-search:refnum",
221+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref_union"
222+
+ ]
223+
+ refs = self.ctx.find_backlinks_paths(
224+
+ match_path="/yolo-leafref-search:my_list",
225+
+ match_ancestors=True
226+
+ )
227+
+ expected.sort()
228+
+ refs.sort()
229+
+ self.assertEqual(expected, refs)
230+
+ def test_backlinks_leafref_target_paths(self):
231+
+ expected = [
232+
+ "/yolo-leafref-search:my_list/my_leaf_string"
233+
+ ]
234+
+ refs = self.ctx.find_leafref_path_target_paths(
235+
+ "/yolo-leafref-search-extmod:my_extref_list/my_extref"
236+
+ )
237+
+ expected.sort()
238+
+ refs.sort()
239+
+ self.assertEqual(expected, refs)
240+
+
241+
+
242+
# -------------------------------------------------------------------------------------
243+
class ChoiceTest(unittest.TestCase):
244+
def setUp(self):
245+
diff --git a/tests/yang/yolo/yolo-leafref-search-extmod.yang b/tests/yang/yolo/yolo-leafref-search-extmod.yang
246+
new file mode 100644
247+
index 0000000..046ceec
248+
--- /dev/null
249+
+++ b/tests/yang/yolo/yolo-leafref-search-extmod.yang
250+
@@ -0,0 +1,39 @@
251+
+module yolo-leafref-search-extmod {
252+
+ yang-version 1.1;
253+
+ namespace "urn:yang:yolo:leafref-search-extmod";
254+
+ prefix leafref-search-extmod;
255+
+
256+
+ import wtf-types { prefix types; }
257+
+
258+
+ import yolo-leafref-search {
259+
+ prefix leafref-search;
260+
+ }
261+
+
262+
+ revision 2025-02-11 {
263+
+ description
264+
+ "Initial version.";
265+
+ }
266+
+
267+
+ list my_extref_list {
268+
+ key my_leaf_string;
269+
+ leaf my_leaf_string {
270+
+ type string;
271+
+ }
272+
+ leaf my_extref {
273+
+ type leafref {
274+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
275+
+ }
276+
+ }
277+
+ leaf my_extref_union {
278+
+ type union {
279+
+ type leafref {
280+
+ path "/leafref-search:my_list/leafref-search:my_leaf_string";
281+
+ }
282+
+ type leafref {
283+
+ path "/leafref-search:my_list/leafref-search:my_leaf_number";
284+
+ }
285+
+ type types:number;
286+
+ }
287+
+ }
288+
+ }
289+
+}
290+
diff --git a/tests/yang/yolo/yolo-leafref-search.yang b/tests/yang/yolo/yolo-leafref-search.yang
291+
new file mode 100644
292+
index 0000000..5f4af48
293+
--- /dev/null
294+
+++ b/tests/yang/yolo/yolo-leafref-search.yang
295+
@@ -0,0 +1,36 @@
296+
+module yolo-leafref-search {
297+
+ yang-version 1.1;
298+
+ namespace "urn:yang:yolo:leafref-search";
299+
+ prefix leafref-search;
300+
+
301+
+ import wtf-types { prefix types; }
302+
+
303+
+ revision 2025-02-11 {
304+
+ description
305+
+ "Initial version.";
306+
+ }
307+
+
308+
+ list my_list {
309+
+ key my_leaf_string;
310+
+ leaf my_leaf_string {
311+
+ type string;
312+
+ }
313+
+ leaf my_leaf_number {
314+
+ description
315+
+ "A number.";
316+
+ type types:number;
317+
+ }
318+
+ }
319+
+
320+
+ leaf refstr {
321+
+ type leafref {
322+
+ path "../my_list/my_leaf_string";
323+
+ }
324+
+ }
325+
+
326+
+ leaf refnum {
327+
+ type leafref {
328+
+ path "../my_list/my_leaf_number";
329+
+ }
330+
+ }
331+
+}

src/libyang3-py3/patch/series

Lines changed: 1 addition & 0 deletions
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)