From 18faa1e71e67ab81c65ef7450c6ea68976633368 Mon Sep 17 00:00:00 2001 From: Brad House Date: Sun, 16 Feb 2025 10:18:20 -0500 Subject: [PATCH 01/16] data: option to allow json int/bool as strings Depends on https://github.com/CESNET/libyang/pull/2344 Add support for new option to python bindings. Signed-off-by: Brad House --- cffi/cdefs.h | 1 + cffi/source.c | 4 ++-- libyang/context.py | 6 ++++++ libyang/data.py | 3 +++ pylintrc | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cffi/cdefs.h b/cffi/cdefs.h index aa750042..de05f4a1 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -321,6 +321,7 @@ LY_ERR lyd_print_all(struct ly_out *, const struct lyd_node *, LYD_FORMAT, uint3 #define LYD_PARSE_OPTS_MASK ... #define LYD_PARSE_ORDERED ... #define LYD_PARSE_STRICT ... +#define LYD_PARSE_JSON_STRING_DATATYPES ... #define LYD_VALIDATE_NO_STATE ... #define LYD_VALIDATE_PRESENT ... diff --git a/cffi/source.c b/cffi/source.c index b54ba0de..34e18b0e 100644 --- a/cffi/source.c +++ b/cffi/source.c @@ -6,6 +6,6 @@ #include #include -#if (LY_VERSION_MAJOR != 3) -#error "This version of libyang bindings only works with libyang 3.x" +#if LY_VERSION_MAJOR * 10000 + LY_VERSION_MINOR * 100 + LY_VERSION_MICRO < 30801 +#error "This version of libyang bindings only works with libyang soversion 3.8.1+" #endif diff --git a/libyang/context.py b/libyang/context.py index f9bd5a57..b50600de 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -533,6 +533,7 @@ def parse_data( validate_multi_error: bool = False, store_only: bool = False, json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: if self.cdata is None: raise RuntimeError("context already destroyed") @@ -545,6 +546,7 @@ def parse_data( strict=strict, store_only=store_only, json_null=json_null, + json_string_datatypes=json_string_datatypes, ) validation_flgs = validation_flags( no_state=no_state, @@ -604,6 +606,7 @@ def parse_data_mem( validate_multi_error: bool = False, store_only: bool = False, json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: return self.parse_data( fmt, @@ -620,6 +623,7 @@ def parse_data_mem( validate_multi_error=validate_multi_error, store_only=store_only, json_null=json_null, + json_string_datatypes=json_string_datatypes, ) def parse_data_file( @@ -637,6 +641,7 @@ def parse_data_file( validate_multi_error: bool = False, store_only: bool = False, json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: return self.parse_data( fmt, @@ -653,6 +658,7 @@ def parse_data_file( validate_multi_error=validate_multi_error, store_only=store_only, json_null=json_null, + json_string_datatypes=json_string_datatypes, ) def __iter__(self) -> Iterator[Module]: diff --git a/libyang/data.py b/libyang/data.py index 9595ea16..2fc086f6 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -117,6 +117,7 @@ def parser_flags( strict: bool = False, store_only: bool = False, json_null: bool = False, + json_string_datatypes: bool = False, ) -> int: flags = 0 if lyb_mod_update: @@ -135,6 +136,8 @@ def parser_flags( flags |= lib.LYD_PARSE_STORE_ONLY if json_null: flags |= lib.LYD_PARSE_JSON_NULL + if json_string_datatypes: + flags |= lib.LYD_PARSE_JSON_STRING_DATATYPES return flags diff --git a/pylintrc b/pylintrc index acf27338..cd4638b7 100644 --- a/pylintrc +++ b/pylintrc @@ -494,7 +494,7 @@ valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method. -max-args=15 +max-args=20 # Maximum number of attributes for a class (see R0902). max-attributes=20 From c8313b76f4ec3c8c03e16e66d1f4f229e31553fe Mon Sep 17 00:00:00 2001 From: Brad House Date: Sun, 16 Feb 2025 11:04:50 -0500 Subject: [PATCH 02/16] schema/context: restore some backlinks support In libyang v1 the schema nodes had a backlinks member to be able to look up dependents of the node. SONiC depends on this to provide functionality it uses and it needs to be exposed via the python module. In theory, exposing the 'dfs' functions could make this work, but it would likely be cost prohibitive since walking the tree would be expensive to create a python node for evaluation in native python. Instead this PR depends on the this libyang PR: https://github.com/CESNET/libyang/pull/2352 And adds thin wrappers. This implementation provides 2 python functions: * Context.find_backlinks_paths() - This function can take the path of the base node and find all dependents. If no path is specified, then it will return all nodes that contain a leafref reference. * Context.find_leafref_path_target_paths() - This function takes an xpath, then returns all target nodes the xpath may reference. Typically only one will be returned, but multiples may be in the case of a union. A user can build a cache by combining Context.find_backlinks_paths() with no path set and building a reverse table using Context.find_leafref_path_target_paths() Signed-off-by: Brad House --- cffi/cdefs.h | 2 + libyang/context.py | 110 +++++++++++++++++- tests/test_schema.py | 60 ++++++++++ .../yang/yolo/yolo-leafref-search-extmod.yang | 39 +++++++ tests/yang/yolo/yolo-leafref-search.yang | 36 ++++++ 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tests/yang/yolo/yolo-leafref-search-extmod.yang create mode 100644 tests/yang/yolo/yolo-leafref-search.yang diff --git a/cffi/cdefs.h b/cffi/cdefs.h index de05f4a1..4ccc30e7 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -862,6 +862,8 @@ const struct lysc_node* lys_find_child(const struct lysc_node *, const struct ly const struct lysc_node* lysc_node_child(const struct lysc_node *); const struct lysc_node_action* lysc_node_actions(const struct lysc_node *); const struct lysc_node_notif* lysc_node_notifs(const struct lysc_node *); +LY_ERR lysc_node_lref_targets(const struct lysc_node *, struct ly_set **); +LY_ERR lysc_node_lref_backlinks(const struct ly_ctx *, const struct lysc_node *, ly_bool, struct ly_set **); typedef enum { LYD_PATH_STD, diff --git a/libyang/context.py b/libyang/context.py index b50600de..d8dac48a 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: MIT import os -from typing import IO, Any, Callable, Iterator, Optional, Sequence, Tuple, Union +from typing import IO, Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union from _libyang import ffi, lib from .data import ( @@ -661,6 +661,114 @@ def parse_data_file( json_string_datatypes=json_string_datatypes, ) + def find_leafref_path_target_paths(self, leafref_path: str) -> List[str]: + """ + Fetch all leafref targets of the specified path + + This is an enhanced version of lysc_node_lref_target() which will return + a set of leafref target paths retrieved from the specified schema path. + While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and + LYS_LEAFLIST this function will also evaluate other datatypes that may + contain leafrefs such as LYS_UNION. This does not, however, search for + children with leafref targets. + + :arg self + This instance on context + :arg leafref_path: + Path to node to search for leafref targets + :returns List of target paths that the leafrefs of the specified node + point to. + """ + if self.cdata is None: + raise RuntimeError("context already destroyed") + if leafref_path is None: + raise RuntimeError("leafref_path must be defined") + + out = [] + + node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0) + if node == ffi.NULL: + raise self.error("leafref_path not found") + + node_set = ffi.new("struct ly_set **") + if ( + lib.lysc_node_lref_targets(node, node_set) != lib.LY_SUCCESS + or node_set[0] == ffi.NULL + or node_set[0].count == 0 + ): + raise self.error("leafref_path does not contain any leafref targets") + + node_set = node_set[0] + for i in range(node_set.count): + path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0) + out.append(c2str(path)) + lib.free(path) + + lib.ly_set_free(node_set, ffi.NULL) + + return out + + def find_backlinks_paths( + self, match_path: str = None, match_ancestors: bool = False + ) -> List[str]: + """ + Search entire schema for nodes that contain leafrefs and return as a + list of schema node paths. + + Perform a complete scan of the schema tree looking for nodes that + contain leafref entries. When a node contains a leafref entry, and + match_path is specified, determine if reference points to match_path, + if so add the node's path to returned list. If no match_path is + specified, the node containing the leafref is always added to the + returned set. When match_ancestors is true, will evaluate if match_path + is self or an ansestor of self. + + This does not return the leafref targets, but the actual node that + contains a leafref. + + :arg self + This instance on context + :arg match_path: + Target path to use for matching + :arg match_ancestors: + Whether match_path is a base ancestor or an exact node + :returns List of paths. Exception of match_path is not found or if no + backlinks are found. + """ + if self.cdata is None: + raise RuntimeError("context already destroyed") + out = [] + + match_node = ffi.NULL + if match_path is not None and match_path == "/" or match_path == "": + match_path = None + + if match_path: + match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0) + if match_node == ffi.NULL: + raise self.error("match_path not found") + + node_set = ffi.new("struct ly_set **") + if ( + lib.lysc_node_lref_backlinks( + self.cdata, match_node, match_ancestors, node_set + ) + != lib.LY_SUCCESS + or node_set[0] == ffi.NULL + or node_set[0].count == 0 + ): + raise self.error("backlinks not found") + + node_set = node_set[0] + for i in range(node_set.count): + path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0) + out.append(c2str(path)) + lib.free(path) + + lib.ly_set_free(node_set, ffi.NULL) + + return out + def __iter__(self) -> Iterator[Module]: """ Return an iterator that yields all implemented modules from the context diff --git a/tests/test_schema.py b/tests/test_schema.py index a310aadc..b3feba3a 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -801,6 +801,66 @@ def test_leaf_list_parsed(self): self.assertFalse(pnode.ordered()) +# ------------------------------------------------------------------------------------- +class BacklinksTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-leafref-search") + self.ctx.load_module("yolo-leafref-search-extmod") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_backlinks_all_nodes(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search:refnum", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths() + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_one(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths( + match_path="/yolo-leafref-search:my_list/my_leaf_string" + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_children(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search:refnum", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths( + match_path="/yolo-leafref-search:my_list", match_ancestors=True + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_leafref_target_paths(self): + expected = ["/yolo-leafref-search:my_list/my_leaf_string"] + refs = self.ctx.find_leafref_path_target_paths( + "/yolo-leafref-search-extmod:my_extref_list/my_extref" + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + # ------------------------------------------------------------------------------------- class ChoiceTest(unittest.TestCase): def setUp(self): diff --git a/tests/yang/yolo/yolo-leafref-search-extmod.yang b/tests/yang/yolo/yolo-leafref-search-extmod.yang new file mode 100644 index 00000000..046ceec5 --- /dev/null +++ b/tests/yang/yolo/yolo-leafref-search-extmod.yang @@ -0,0 +1,39 @@ +module yolo-leafref-search-extmod { + yang-version 1.1; + namespace "urn:yang:yolo:leafref-search-extmod"; + prefix leafref-search-extmod; + + import wtf-types { prefix types; } + + import yolo-leafref-search { + prefix leafref-search; + } + + revision 2025-02-11 { + description + "Initial version."; + } + + list my_extref_list { + key my_leaf_string; + leaf my_leaf_string { + type string; + } + leaf my_extref { + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_string"; + } + } + leaf my_extref_union { + type union { + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_string"; + } + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_number"; + } + type types:number; + } + } + } +} diff --git a/tests/yang/yolo/yolo-leafref-search.yang b/tests/yang/yolo/yolo-leafref-search.yang new file mode 100644 index 00000000..5f4af488 --- /dev/null +++ b/tests/yang/yolo/yolo-leafref-search.yang @@ -0,0 +1,36 @@ +module yolo-leafref-search { + yang-version 1.1; + namespace "urn:yang:yolo:leafref-search"; + prefix leafref-search; + + import wtf-types { prefix types; } + + revision 2025-02-11 { + description + "Initial version."; + } + + list my_list { + key my_leaf_string; + leaf my_leaf_string { + type string; + } + leaf my_leaf_number { + description + "A number."; + type types:number; + } + } + + leaf refstr { + type leafref { + path "../my_list/my_leaf_string"; + } + } + + leaf refnum { + type leafref { + path "../my_list/my_leaf_number"; + } + } +} From 91067ebd3d39bd628db7fdd6229e97f5318c5f4d Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sat, 5 Jul 2025 06:15:52 -0400 Subject: [PATCH 03/16] context: use normal type hint rather than forward reference SNode is imported and available so there's no need to use the "libyang.SNode" forward reference form. (based)pyright complains about this as it considers these forward references as `Unknown` -- perhaps it should be smarter, but that's a different issue. Signed-off-by: Christian Hopps --- libyang/context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libyang/context.py b/libyang/context.py index d8dac48a..33580d61 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -371,7 +371,7 @@ def find_path( self, path: str, output: bool = False, - root_node: Optional["libyang.SNode"] = None, + root_node: Optional[SNode] = None, ) -> Iterator[SNode]: if self.cdata is None: raise RuntimeError("context already destroyed") @@ -404,9 +404,9 @@ def find_path( def find_jsonpath( self, path: str, - root_node: Optional["libyang.SNode"] = None, + root_node: Optional[SNode] = None, output: bool = False, - ) -> Optional["libyang.SNode"]: + ) -> Optional[SNode]: if root_node is not None: ctx_node = root_node.cdata else: From 1714370d03440641539787c44921bb90b9edbcd4 Mon Sep 17 00:00:00 2001 From: Brad House Date: Sun, 6 Jul 2025 16:42:00 -0400 Subject: [PATCH 04/16] tests: test_iffeature_state crash fix As observed on Debian Trixie RC2, test_iffeature_state crashes due to an internal pointer being invalidated. This invalidation appears to be due to a call to lys_set_implemented() possibly causing a full recompile of the ctx. This simply reorders the caching of the path pointers so they are not invalidated when used. Signed-off-by: Brad House --- tests/test_schema.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_schema.py b/tests/test_schema.py index b3feba3a..bf4c1737 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -197,18 +197,8 @@ def feature_disable_only(feature): continue self.mod.feature_enable(f.name()) - leaf_simple = next(self.ctx.find_path("/yolo-system:conf/yolo-system:speed")) - - self.mod.feature_disable_all() - leaf_not = next(self.ctx.find_path("/yolo-system:conf/yolo-system:offline")) - self.mod.feature_enable_all() - - leaf_and = next(self.ctx.find_path("/yolo-system:conf/yolo-system:full")) - leaf_or = next( - self.ctx.find_path("/yolo-system:conf/yolo-system:isolation-level") - ) - # if-feature is just a feature + leaf_simple = next(self.ctx.find_path("/yolo-system:conf/yolo-system:speed")) tree = next(leaf_simple.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) @@ -216,6 +206,8 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), False) # if-feature is "NOT networking" + self.mod.feature_disable_all() + leaf_not = next(self.ctx.find_path("/yolo-system:conf/yolo-system:offline")) tree = next(leaf_not.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), False) @@ -223,6 +215,8 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), True) # if-feature is "turbo-boost AND networking" + self.mod.feature_enable_all() + leaf_and = next(self.ctx.find_path("/yolo-system:conf/yolo-system:full")) tree = next(leaf_and.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) @@ -234,6 +228,9 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), False) # if-feature is "turbo-boost OR networking" + leaf_or = next( + self.ctx.find_path("/yolo-system:conf/yolo-system:isolation-level") + ) tree = next(leaf_or.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) From aa3f506cf9119e5448a257bbd62fe679c5896246 Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sun, 31 Aug 2025 01:45:46 +0000 Subject: [PATCH 05/16] data: add missing DNode.is_default API Add access to libyang lyd_is_default() API. Signed-off-by: Christian Hopps --- cffi/cdefs.h | 1 + libyang/data.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 4ccc30e7..f714e3ba 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -944,6 +944,7 @@ struct lyd_value_union { const char * lyd_get_value(const struct lyd_node *); struct lyd_node* lyd_child(const struct lyd_node *); +ly_bool lyd_is_default(const struct lyd_node *); LY_ERR lyd_find_path(const struct lyd_node *, const char *, ly_bool, struct lyd_node **); void lyd_free_siblings(struct lyd_node *); struct lyd_node* lyd_first_sibling(const struct lyd_node *); diff --git a/libyang/data.py b/libyang/data.py index 2fc086f6..facf932e 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -390,6 +390,9 @@ def flags(self): ret["new"] = True return ret + def is_default(self) -> bool: + return lib.lyd_is_default(self.cdata) + def set_when(self, value: bool): if value: self.cdata.flags |= lib.LYD_WHEN_TRUE From e977f94a319c65a92011cff2dc39903ae4c848dc Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sun, 31 Aug 2025 01:56:47 +0000 Subject: [PATCH 06/16] data: add missing DataType's Simplify the code to using python enum functionality. Signed-off-by: Christian Hopps --- cffi/cdefs.h | 5 ++++- libyang/data.py | 20 ++++---------------- libyang/util.py | 17 ++++++++++------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/cffi/cdefs.h b/cffi/cdefs.h index f714e3ba..06f938b3 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -296,7 +296,10 @@ enum lyd_type { LYD_TYPE_REPLY_YANG, LYD_TYPE_RPC_NETCONF, LYD_TYPE_NOTIF_NETCONF, - LYD_TYPE_REPLY_NETCONF + LYD_TYPE_REPLY_NETCONF, + LYD_TYPE_RPC_RESTCONF, + LYD_TYPE_NOTIF_RESTCONF, + LYD_TYPE_REPLY_RESTCONF }; #define LYD_PRINT_KEEPEMPTYCONT ... diff --git a/libyang/data.py b/libyang/data.py index facf932e..540ce39c 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -177,22 +177,10 @@ def dup_flags( # ------------------------------------------------------------------------------------- -def data_type(dtype): - if dtype == DataType.DATA_YANG: - return lib.LYD_TYPE_DATA_YANG - if dtype == DataType.RPC_YANG: - return lib.LYD_TYPE_RPC_YANG - if dtype == DataType.NOTIF_YANG: - return lib.LYD_TYPE_NOTIF_YANG - if dtype == DataType.REPLY_YANG: - return lib.LYD_TYPE_REPLY_YANG - if dtype == DataType.RPC_NETCONF: - return lib.LYD_TYPE_RPC_NETCONF - if dtype == DataType.NOTIF_NETCONF: - return lib.LYD_TYPE_NOTIF_NETCONF - if dtype == DataType.REPLY_NETCONF: - return lib.LYD_TYPE_REPLY_NETCONF - raise ValueError("Unknown data type") +def data_type(dtype: DataType) -> int: + if not isinstance(dtype, DataType): + dtype = DataType(dtype) + return dtype.value # ------------------------------------------------------------------------------------- diff --git a/libyang/util.py b/libyang/util.py index c380ae5e..3014cc85 100644 --- a/libyang/util.py +++ b/libyang/util.py @@ -77,13 +77,16 @@ class IOType(enum.Enum): # ------------------------------------------------------------------------------------- class DataType(enum.Enum): - DATA_YANG = enum.auto() - RPC_YANG = enum.auto() - NOTIF_YANG = enum.auto() - REPLY_YANG = enum.auto() - RPC_NETCONF = enum.auto() - NOTIF_NETCONF = enum.auto() - REPLY_NETCONF = enum.auto() + DATA_YANG = lib.LYD_TYPE_DATA_YANG + RPC_YANG = lib.LYD_TYPE_RPC_YANG + NOTIF_YANG = lib.LYD_TYPE_NOTIF_YANG + REPLY_YANG = lib.LYD_TYPE_REPLY_YANG + RPC_NETCONF = lib.LYD_TYPE_RPC_NETCONF + NOTIF_NETCONF = lib.LYD_TYPE_NOTIF_NETCONF + REPLY_NETCONF = lib.LYD_TYPE_REPLY_NETCONF + RPC_RESTCONF = lib.LYD_TYPE_RPC_RESTCONF + NOTIF_RESTCONF = lib.LYD_TYPE_NOTIF_RESTCONF + REPLY_RESTCONF = lib.LYD_TYPE_REPLY_RESTCONF # ------------------------------------------------------------------------------------- From 2740fa2ac3272683c362651c55b7f7dd2eae3911 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Tue, 16 Sep 2025 12:49:09 +0200 Subject: [PATCH 07/16] data: adding ability to get target_nodes This patch introduces target_nodes API, which allows user to get all target data nodes that current leafref node is pointing to Signed-off-by: Stefan Gula --- libyang/data.py | 15 ++++++++++++++- tests/test_data.py | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/libyang/data.py b/libyang/data.py index 540ce39c..ceb1fa7b 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -1009,7 +1009,7 @@ def leafref_link_node_tree(self) -> None: def leafref_nodes(self) -> Iterator["DNode"]: """ - Gets the leafref links record for given node. + Gets the nodes that are referring to this node. Requires leafref_linking to be set on the libyang context. """ @@ -1020,6 +1020,19 @@ def leafref_nodes(self) -> Iterator["DNode"]: for n in ly_array_iter(out[0].leafref_nodes): yield DNode.new(self.context, n) + def target_nodes(self) -> Iterator["DNode"]: + """ + Gets the target nodes that are referred by this node. + + Requires leafref_linking to be set on the libyang context. + """ + term_node = ffi.cast("struct lyd_node_term *", self.cdata) + out = ffi.new("const struct lyd_leafref_links_rec **") + if lib.lyd_leafref_get_links(term_node, out) != lib.LY_SUCCESS: + return + for n in ly_array_iter(out[0].target_nodes): + yield DNode.new(self.context, n) + def __repr__(self): cls = self.__class__ return "<%s.%s: %s>" % (cls.__module__, cls.__name__, str(self)) diff --git a/tests/test_data.py b/tests/test_data.py index 1479eb9a..820db0ca 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1092,6 +1092,9 @@ def test_dnode_leafref_linking(self): dnode4 = next(dnode3.leafref_nodes()) self.assertIsInstance(dnode4, DLeaf) self.assertEqual(dnode4.cdata, dnode2.cdata) + dnode5 = next(dnode4.target_nodes()) + self.assertIsInstance(dnode5, DLeaf) + self.assertEqual(dnode5.cdata, dnode3.cdata) dnode1.free() def test_dnode_store_only(self): From 39afe5e72f6a876df046da3a318eaa4ecb263449 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Tue, 16 Sep 2025 12:44:15 +0200 Subject: [PATCH 08/16] data: adding dep_tree option for RPC validation This patch introduces dep_tree option to provide config data to which the RPC can referenced to during validation Signed-off-by: Stefan Gula --- libyang/data.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libyang/data.py b/libyang/data.py index ceb1fa7b..7a5887b0 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -526,6 +526,7 @@ def validate( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = None if rpc: @@ -538,7 +539,7 @@ def validate( if dtype is None: self.validate_all(no_state, validate_present) else: - self.validate_op(dtype) + self.validate_op(dtype, dep_tree) def validate_all( self, @@ -560,11 +561,15 @@ def validate_all( def validate_op( self, dtype: DataType, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = data_type(dtype) - node_p = ffi.new("struct lyd_node **") - node_p[0] = self.cdata - ret = lib.lyd_validate_op(node_p[0], ffi.NULL, dtype, ffi.NULL) + ret = lib.lyd_validate_op( + self.cdata, + ffi.NULL if dep_tree is None else dep_tree.cdata, + dtype, + ffi.NULL, + ) if ret != lib.LY_SUCCESS: raise self.context.error("validation failed") From 41470168b69b3852525b2b213ec0a4f206e9246b Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Tue, 16 Sep 2025 12:23:41 +0200 Subject: [PATCH 09/16] schema: adds ability to use with_choice option in children calls This patch adds ability to use with_choice within RPC and Notification Signed-off-by: Stefan Gula --- libyang/schema.py | 28 +++++++++++++++++++++------- tests/test_schema.py | 2 ++ tests/yang/yolo/yolo-system.yang | 9 ++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/libyang/schema.py b/libyang/schema.py index 3f3bd4d0..76fc9c8f 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -1679,8 +1679,12 @@ class SRpcInOut(SNode): def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @@ -1710,11 +1714,17 @@ def output(self) -> Optional[SRpcInOut]: def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - yield from iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + yield from iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # With libyang2, you can get only input or output # To keep behavior, we iter 2 times witt output options - yield from iter_children(self.context, self.cdata, types=types, output=True) + yield from iter_children( + self.context, self.cdata, types=types, output=True, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @@ -1723,8 +1733,12 @@ class SNotif(SNode): def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- diff --git a/tests/test_schema.py b/tests/test_schema.py index bf4c1737..d9824e58 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -491,6 +491,8 @@ def test_rpc_attrs(self): self.assertEqual(self.rpc.nodetype(), SNode.RPC) self.assertEqual(self.rpc.keyword(), "rpc") self.assertEqual(self.rpc.schema_path(), "/yolo-system:format-disk") + choice = next(self.rpc.input().children((SNode.CHOICE,), with_choice=True)) + self.assertIsInstance(choice, SChoice) def test_rpc_extensions(self): ext = list(self.rpc.extensions()) diff --git a/tests/yang/yolo/yolo-system.yang b/tests/yang/yolo/yolo-system.yang index 36c76416..b48dcbe8 100644 --- a/tests/yang/yolo/yolo-system.yang +++ b/tests/yang/yolo/yolo-system.yang @@ -191,7 +191,14 @@ module yolo-system { ext:human-name "Disk"; type types:str; } - anyxml html-info; + choice xml-or-json { + case xml { + anyxml html-info; + } + case json { + anydata json-info; + } + } } output { leaf duration { From d3dd5f017f6f1fc4ee818a55ec77fe055795c991 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Mon, 22 Sep 2025 00:52:49 +0200 Subject: [PATCH 10/16] schema: adds ability to get various outputs as extension parent_node This patch adds ability to get also PRefine, PType and PEnum as a valid output using extension parent_node function Signed-off-by: Stefan Gula --- libyang/schema.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libyang/schema.py b/libyang/schema.py index 76fc9c8f..9b507ab6 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -423,10 +423,21 @@ def name(self) -> str: def module(self) -> Module: return self._module_from_parsed() - def parent_node(self) -> Optional[Union["PNode", "PIdentity"]]: + def parent_node( + self, + ) -> Optional[Union["PNode", "PIdentity", "PRefine", "PType", "PEnum"]]: if self.cdata.parent_stmt == lib.LY_STMT_IDENTITY: cdata = ffi.cast("struct lysp_ident *", self.cdata.parent) return PIdentity(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_REFINE: + cdata = ffi.cast("struct lysp_refine *", self.cdata.parent) + return PRefine(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_TYPE: + cdata = ffi.cast("struct lysp_type *", self.cdata.parent) + return PType(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_ENUM: + cdata = ffi.cast("struct lysp_type_enum *", self.cdata.parent) + return PEnum(self.context, cdata, self.module_parent) if bool(self.cdata.parent_stmt & lib.LY_STMT_NODE_MASK): try: return PNode.new(self.context, self.cdata.parent, self.module_parent) From 90dad07385ea75c1464414357d0926f083fed0ce Mon Sep 17 00:00:00 2001 From: Pepa Hajek Date: Mon, 22 Sep 2025 16:29:39 +0200 Subject: [PATCH 11/16] data: add structured error in exception Refactor LibyangError exception handling with structured details Signed-off-by: Pepa Hajek --- libyang/context.py | 42 +++++++++++++++++++++++++++++++----------- libyang/util.py | 22 ++++++++++++++++++++-- tests/test_data.py | 12 ++++++++++++ 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/libyang/context.py b/libyang/context.py index 33580d61..e79f2e2c 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -16,7 +16,15 @@ validation_flags, ) from .schema import Module, SNode, schema_in_format -from .util import DataType, IOType, LibyangError, c2str, data_load, str2c +from .util import ( + DataType, + IOType, + LibyangError, + LibyangErrorItem, + c2str, + data_load, + str2c, +) # ------------------------------------------------------------------------------------- @@ -284,23 +292,35 @@ def __exit__(self, *args, **kwargs): self.destroy() def error(self, msg: str, *args) -> LibyangError: - msg %= args + if args: + msg = msg % args + + parts = [msg] + errors = [] if self.cdata: err = lib.ly_err_first(self.cdata) while err: - if err.msg: - msg += ": %s" % c2str(err.msg) - if err.data_path: - msg += ": Data path: %s" % c2str(err.data_path) - if err.schema_path: - msg += ": Schema path: %s" % c2str(err.schema_path) - if err.line != 0: - msg += " (line %u)" % err.line + m = c2str(err.msg) if err.msg else None + dp = c2str(err.data_path) if err.data_path else None + sp = c2str(err.schema_path) if err.schema_path else None + ln = int(err.line) if err.line else None + parts.extend( + tmpl.format(val) + for val, tmpl in [ + (m, ": {}"), + (dp, ": Data path: {}"), + (sp, ": Schema path: {}"), + (ln, " (line {})"), + ] + if val is not None + ) + errors.append(LibyangErrorItem(m, dp, sp, ln)) err = err.next lib.ly_err_clean(self.cdata, ffi.NULL) - return LibyangError(msg) + msg = "".join(parts) + return LibyangError(msg, errors=errors) def parse_module( self, diff --git a/libyang/util.py b/libyang/util.py index 3014cc85..7b272692 100644 --- a/libyang/util.py +++ b/libyang/util.py @@ -2,16 +2,34 @@ # Copyright (c) 2021 RACOM s.r.o. # SPDX-License-Identifier: MIT +from dataclasses import dataclass import enum -from typing import Optional +from typing import Iterable, Optional import warnings from _libyang import ffi, lib +# ------------------------------------------------------------------------------------- +@dataclass(frozen=True) +class LibyangErrorItem: + msg: Optional[str] + data_path: Optional[str] + schema_path: Optional[str] + line: Optional[int] + + # ------------------------------------------------------------------------------------- class LibyangError(Exception): - pass + def __init__( + self, message: str, *args, errors: Optional[Iterable[LibyangErrorItem]] = None + ): + super().__init__(message, *args) + self.message = message + self.errors = tuple(errors or ()) + + def __str__(self): + return self.message # ------------------------------------------------------------------------------------- diff --git a/tests/test_data.py b/tests/test_data.py index 820db0ca..430873a7 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -289,6 +289,18 @@ def test_data_parse_config_xml_multi_error(self): "Data path: /yolo-system:conf/url[proto='https'] (line 7)", ) + first = cm.exception.errors[0] + self.assertEqual(first.msg, 'Invalid boolean value "abcd".') + self.assertEqual( + first.data_path, "/yolo-system:conf/url[proto='https']/enabled" + ) + self.assertEqual(first.line, 6) + + second = cm.exception.errors[1] + self.assertEqual(second.msg, 'List instance is missing its key "host".') + self.assertEqual(second.data_path, "/yolo-system:conf/url[proto='https']") + self.assertEqual(second.line, 7) + XML_STATE = """ foo From b78cdce6c320b6f83a99b015d57e5e233bf5e30f Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sun, 12 Oct 2025 02:06:22 +0000 Subject: [PATCH 12/16] schema: add missing extensions() for access to module level Currently only have access to the extensions used under schema nodes, need access to extensions used at the module level. Add a test case for the functionality as well. Signed-off-by: Christian Hopps --- cffi/cdefs.h | 8 ++++++++ libyang/schema.py | 23 +++++++++++++++++++++++ tests/test_schema.py | 13 +++++++++++++ tests/yang/yolo/yolo-system.yang | 4 ++++ 4 files changed, 48 insertions(+) diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 06f938b3..671d8cb8 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -365,6 +365,14 @@ LY_ERR lys_print_module(struct ly_out *, const struct lys_module *, LYS_OUTFORMA #define LYS_PRINT_NO_SUBSTMT ... #define LYS_PRINT_SHRINK ... +struct lysc_module { + struct lys_module *mod; + struct lysc_node *data; + struct lysc_node_action *rpcs; + struct lysc_node_notif *notifs; + struct lysc_ext_instance *exts; +}; + struct lys_module { struct ly_ctx *ctx; const char *name; diff --git a/libyang/schema.py b/libyang/schema.py index 9b507ab6..82a1fee1 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -180,6 +180,29 @@ def parsed_identities(self) -> Iterator["PIdentity"]: for i in ly_array_iter(self.cdata.parsed.identities): yield PIdentity(self.context, i, self) + def extensions(self) -> Iterator["ExtensionCompiled"]: + compiled = ffi.cast("struct lysc_module *", self.cdata.compiled) + if compiled == ffi.NULL: + return + exts = ffi.cast("struct lysc_ext_instance *", self.cdata.compiled.exts) + if exts == ffi.NULL: + return + for extension in ly_array_iter(exts): + yield ExtensionCompiled(self.context, extension) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional["ExtensionCompiled"]: + for ext in self.extensions(): + if ext.name() != name: + continue + if prefix is not None and ext.module().name() != prefix: + continue + if arg_value is not None and ext.argument() != arg_value: + continue + return ext + return None + def __str__(self) -> str: return self.name() diff --git a/tests/test_schema.py b/tests/test_schema.py index d9824e58..0a5347ff 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -129,6 +129,19 @@ def test_mod_revisions(self): self.assertEqual(revisions[0].date(), "1999-04-01") self.assertEqual(revisions[1].date(), "1990-04-01") + def test_mod_extensions(self): + assert self.module is not None # pyright doesn't understand assertIsNotNone() + exts = list(self.module.extensions()) + self.assertEqual(len(exts), 1) + ext = self.module.get_extension("compile-validation", prefix="omg-extensions") + self.assertEqual(ext.argument(), "module-level") + sub_exts = list(ext.extensions()) + self.assertEqual(len(sub_exts), 1) + ext = sub_exts[0] + self.assertEqual(ext.name(), "compile-validation") + self.assertEqual(ext.module().name(), "omg-extensions") + self.assertEqual(ext.argument(), "module-sub-level") + # ------------------------------------------------------------------------------------- class RevisionTest(unittest.TestCase): diff --git a/tests/yang/yolo/yolo-system.yang b/tests/yang/yolo/yolo-system.yang index b48dcbe8..a02f310c 100644 --- a/tests/yang/yolo/yolo-system.yang +++ b/tests/yang/yolo/yolo-system.yang @@ -155,6 +155,10 @@ module yolo-system { } } + ext:compile-validation "module-level" { + ext:compile-validation "module-sub-level"; + } + container conf { description "Configuration."; From 2ff17b4767c9d1b8ec7a4663e047cb797e939f82 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Thu, 16 Oct 2025 04:11:23 +0200 Subject: [PATCH 13/16] schema: adds extensions and get_extension for Enum class This patch allows to get extensions on Enum type Signed-off-by: Stefan Gula --- libyang/schema.py | 17 +++++++++++++++++ tests/test_schema.py | 3 +++ 2 files changed, 20 insertions(+) diff --git a/libyang/schema.py b/libyang/schema.py index 82a1fee1..c9f2a5ef 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -525,6 +525,23 @@ def name(self) -> str: def description(self) -> str: return c2str(self.cdata.dsc) + def extensions(self) -> Iterator[ExtensionCompiled]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionCompiled(self.context, ext) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional[ExtensionCompiled]: + for ext in self.extensions(): + if ext.name() != name: + continue + if prefix is not None and ext.module().name() != prefix: + continue + if arg_value is not None and ext.argument() != arg_value: + continue + return ext + return None + def deprecated(self) -> bool: return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) diff --git a/tests/test_schema.py b/tests/test_schema.py index 0a5347ff..e27e0010 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -645,6 +645,9 @@ def test_leaf_type_enum(self): self.assertEqual(t.base(), Type.ENUM) enums = [e.name() for e in t.enums()] self.assertEqual(enums, ["http", "https", "ftp", "sftp"]) + enum = next(t.enums()) + self.assertIsNone(next(enum.extensions(), None)) + self.assertIsNone(enum.get_extension("test", prefix="test")) def test_leaf_type_bits(self): leaf = next(self.ctx.find_path("/yolo-system:chmod/yolo-system:perms")) From b35336ba4950a36a0dd9851b61bae6d8d2962bd6 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Thu, 16 Oct 2025 04:04:31 +0200 Subject: [PATCH 14/16] context: adds find_xpath_atoms function This patch is adding find_xpath_atoms on context level to allow users to get all nodes needed for xpath evaluation Signed-off-by: Stefan Gula --- cffi/cdefs.h | 1 + libyang/context.py | 32 ++++++++++++++++++++++++++++++++ tests/test_context.py | 18 +++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 671d8cb8..52f00b24 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -215,6 +215,7 @@ struct lys_module* ly_ctx_get_module_latest(const struct ly_ctx *, const char *) LY_ERR ly_ctx_compile(struct ly_ctx *); LY_ERR lys_find_xpath(const struct ly_ctx *, const struct lysc_node *, const char *, uint32_t, struct ly_set **); +LY_ERR lys_find_xpath_atoms(const struct ly_ctx *, const struct lysc_node *, const char *, uint32_t, struct ly_set **); void ly_set_free(struct ly_set *, void(*)(void *obj)); struct ly_set { diff --git a/libyang/context.py b/libyang/context.py index e79f2e2c..05ad6ec4 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -421,6 +421,38 @@ def find_path( finally: lib.ly_set_free(node_set, ffi.NULL) + def find_xpath_atoms( + self, + path: str, + output: bool = False, + root_node: Optional["libyang.SNode"] = None, + ) -> Iterator[SNode]: + if self.cdata is None: + raise RuntimeError("context already destroyed") + + if root_node is not None: + ctx_node = root_node.cdata + else: + ctx_node = ffi.NULL + + flags = lib.LYS_FIND_XP_OUTPUT if output else 0 + + node_set = ffi.new("struct ly_set **") + if ( + lib.lys_find_xpath_atoms(self.cdata, ctx_node, str2c(path), flags, node_set) + != lib.LY_SUCCESS + ): + raise self.error("cannot find path") + + node_set = node_set[0] + if node_set.count == 0: + raise self.error("cannot find path") + try: + for i in range(node_set.count): + yield SNode.new(self, node_set.snodes[i]) + finally: + lib.ly_set_free(node_set, ffi.NULL) + def find_jsonpath( self, path: str, diff --git a/tests/test_context.py b/tests/test_context.py index db03c329..61c0ae8c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,7 +4,7 @@ import os import unittest -from libyang import Context, LibyangError, Module, SLeaf, SLeafList +from libyang import Context, LibyangError, Module, SContainer, SLeaf, SLeafList from libyang.util import c2str @@ -95,6 +95,22 @@ def test_ctx_find_path(self): node2 = next(ctx.find_path("../number", root_node=node)) self.assertIsInstance(node2, SLeafList) + def test_ctx_find_xpath_atoms(self): + with Context(YANG_DIR) as ctx: + ctx.load_module("yolo-system") + node_iter = ctx.find_xpath_atoms("/yolo-system:conf/offline") + node = next(node_iter) + self.assertIsInstance(node, SContainer) + node = next(node_iter) + self.assertIsInstance(node, SLeaf) + node_iter = ctx.find_xpath_atoms("../number", root_node=node) + node = next(node_iter) + self.assertIsInstance(node, SLeaf) + node = next(node_iter) + self.assertIsInstance(node, SContainer) + node = next(node_iter) + self.assertIsInstance(node, SLeafList) + def test_ctx_iter_modules(self): with Context(YANG_DIR) as ctx: ctx.load_module("yolo-system") From 74b7d1da4fc82bb575c06055b8189dd5410fec48 Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Tue, 16 Sep 2025 12:41:01 +0200 Subject: [PATCH 15/16] schema: adds ability to get when context nodes This patch adds ability to get context schema node from which when contition is evaluated. It also fixes memory leak of original when_conditions Signed-off-by: Stefan Gula --- libyang/schema.py | 12 +++++++++++- tests/test_schema.py | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/libyang/schema.py b/libyang/schema.py index c9f2a5ef..23c1b1bf 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -1440,13 +1440,23 @@ def parent(self) -> Optional["SNode"]: return None def when_conditions(self): - wh = ffi.new("struct lysc_when **") wh = lib.lysc_node_when(self.cdata) if wh == ffi.NULL: return for cond in ly_array_iter(wh): yield c2str(lib.lyxp_get_expr(cond.cond)) + def when_conditions_nodes(self) -> Iterator[Optional["SNode"]]: + wh = lib.lysc_node_when(self.cdata) + if wh == ffi.NULL: + return + for cond in ly_array_iter(wh): + yield ( + None + if cond.context == ffi.NULL + else SNode.new(self.context, cond.context) + ) + def parsed(self) -> Optional["PNode"]: if self.cdata_parsed is None or self.cdata_parsed == ffi.NULL: return None diff --git a/tests/test_schema.py b/tests/test_schema.py index e27e0010..b1862ab8 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -918,6 +918,14 @@ def tearDown(self): self.ctx.destroy() self.ctx = None + def test_anydata(self): + snode = next(self.ctx.find_path("/yolo-nodetypes:any1")) + self.assertIsInstance(snode, SAnydata) + assert next(snode.when_conditions()) is not None + snode2 = next(snode.when_conditions_nodes()) + assert isinstance(snode2, SAnydata) + assert snode2.cdata == snode.cdata + def test_anydata_parsed(self): snode = next(self.ctx.find_path("/yolo-nodetypes:any1")) self.assertIsInstance(snode, SAnydata) From 2cae616d04c79fa3475e7ce63de450bba402e3ae Mon Sep 17 00:00:00 2001 From: Stefan Gula Date: Thu, 16 Oct 2025 03:52:33 +0200 Subject: [PATCH 16/16] schema: adds libyang.Enum to package This patch adds missing Enum class to __init__.py to allow imports by library users Signed-off-by: Stefan Gula --- libyang/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libyang/__init__.py b/libyang/__init__.py index ff15755c..3d7be2f7 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -67,6 +67,7 @@ from .keyed_list import KeyedList from .log import configure_logging from .schema import ( + Enum, Extension, ExtensionCompiled, ExtensionParsed, @@ -144,6 +145,7 @@ "DefaultRemoved", "DescriptionAdded", "DescriptionRemoved", + "Enum", "EnumAdded", "EnumRemoved", "Extension",