Skip to content

schema/context: restore some backlinks support #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cffi/cdefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,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,
Expand Down
100 changes: 99 additions & 1 deletion libyang/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# SPDX-License-Identifier: MIT

import os
from typing import IO, Any, Callable, Iterator, Optional, Tuple, Union
from typing import IO, Any, Callable, Iterator, Optional, Tuple, Union, List

from _libyang import ffi, lib
from .data import (
Expand Down Expand Up @@ -646,6 +646,104 @@ def parse_data_file(
json_null=json_null,
)

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
Expand Down
58 changes: 58 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,64 @@ 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):
Expand Down
39 changes: 39 additions & 0 deletions tests/yang/yolo/yolo-leafref-search-extmod.yang
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
36 changes: 36 additions & 0 deletions tests/yang/yolo/yolo-leafref-search.yang
Original file line number Diff line number Diff line change
@@ -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";
}
}
}