diff --git a/cffi/cdefs.h b/cffi/cdefs.h index aa75004..52f00b2 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 { @@ -296,7 +297,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 ... @@ -321,6 +325,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 ... @@ -361,6 +366,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; @@ -861,6 +874,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, @@ -941,6 +956,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/cffi/source.c b/cffi/source.c index b54ba0d..34e18b0 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/__init__.py b/libyang/__init__.py index ff15755..3d7be2f 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", diff --git a/libyang/context.py b/libyang/context.py index f9bd5a5..05ad6ec 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 ( @@ -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, @@ -371,7 +391,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") @@ -401,12 +421,44 @@ def find_path( finally: lib.ly_set_free(node_set, ffi.NULL) - def find_jsonpath( + 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, + 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: @@ -533,6 +585,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 +598,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 +658,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 +675,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 +693,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,8 +710,117 @@ 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 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/libyang/data.py b/libyang/data.py index 9595ea1..7a5887b 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 @@ -174,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 # ------------------------------------------------------------------------------------- @@ -387,6 +378,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 @@ -532,6 +526,7 @@ def validate( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = None if rpc: @@ -544,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, @@ -566,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") @@ -1015,7 +1014,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. """ @@ -1026,6 +1025,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/libyang/schema.py b/libyang/schema.py index 3f3bd4d..23c1b1b 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() @@ -423,10 +446,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) @@ -491,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) @@ -1389,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 @@ -1679,8 +1740,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 +1775,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 +1794,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/libyang/util.py b/libyang/util.py index c380ae5..7b27269 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 # ------------------------------------------------------------------------------------- @@ -77,13 +95,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 # ------------------------------------------------------------------------------------- diff --git a/pylintrc b/pylintrc index acf2733..cd4638b 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 diff --git a/tests/test_context.py b/tests/test_context.py index db03c32..61c0ae8 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") diff --git a/tests/test_data.py b/tests/test_data.py index 1479eb9..430873a 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 @@ -1092,6 +1104,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): diff --git a/tests/test_schema.py b/tests/test_schema.py index a310aad..b1862ab 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): @@ -197,18 +210,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 +219,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 +228,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 +241,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) @@ -494,6 +504,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()) @@ -633,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")) @@ -801,6 +816,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): @@ -843,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) 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 0000000..046ceec --- /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 0000000..5f4af48 --- /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"; + } + } +} diff --git a/tests/yang/yolo/yolo-system.yang b/tests/yang/yolo/yolo-system.yang index 36c7641..a02f310 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."; @@ -191,7 +195,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 {