Skip to content

Commit 91db3bd

Browse files
committed
context: adding ContextExternalModuleLoader class
This patch adds class ContextExternalModuleLoader, which adds ability to add custom module load callback, which allows user to load modules from remote source etc. Signed-off-by: Stefan Gula <steweg@gmail.com>
1 parent 8fd20a5 commit 91db3bd

File tree

3 files changed

+199
-2
lines changed

3 files changed

+199
-2
lines changed

cffi/cdefs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,11 @@ typedef enum {
946946
LY_ERR lys_parse(struct ly_ctx *, struct ly_in *, LYS_INFORMAT, const char **, struct lys_module **);
947947
LY_ERR ly_ctx_new_ylpath(const char *, const char *, LYD_FORMAT, int, struct ly_ctx **);
948948
LY_ERR ly_ctx_get_yanglib_data(const struct ly_ctx *, struct lyd_node **, const char *, ...);
949+
typedef void (*ly_module_imp_data_free_clb)(void *, void *);
950+
typedef LY_ERR (*ly_module_imp_clb)(const char *, const char *, const char *, const char *, void *, LYS_INFORMAT *, const char **, ly_module_imp_data_free_clb *);
951+
void ly_ctx_set_module_imp_clb(struct ly_ctx *, ly_module_imp_clb, void *);
952+
extern "Python" void lypy_module_imp_data_free_clb(void *, void *);
953+
extern "Python" LY_ERR lypy_module_imp_clb(const char *, const char *, const char *, const char *, void *, LYS_INFORMAT *, const char **, ly_module_imp_data_free_clb *);
949954

950955
struct lyd_meta {
951956
struct lyd_node *parent;

libyang/context.py

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# SPDX-License-Identifier: MIT
55

66
import os
7-
from typing import IO, Any, Iterator, Optional, Union
7+
from typing import IO, Any, Callable, Iterator, Optional, Tuple, Union
88

99
from _libyang import ffi, lib
1010
from .data import (
@@ -19,9 +19,176 @@
1919
from .util import DataType, IOType, LibyangError, c2str, data_load, str2c
2020

2121

22+
# -------------------------------------------------------------------------------------
23+
@ffi.def_extern(name="lypy_module_imp_data_free_clb")
24+
def libyang_c_module_imp_data_free_clb(cdata, user_data):
25+
instance = ffi.from_handle(user_data)
26+
instance.free_module_data(cdata)
27+
28+
29+
# -------------------------------------------------------------------------------------
30+
@ffi.def_extern(name="lypy_module_imp_clb")
31+
def libyang_c_module_imp_clb(
32+
mod_name,
33+
mod_rev,
34+
submod_name,
35+
submod_rev,
36+
user_data,
37+
fmt,
38+
module_data,
39+
free_module_data,
40+
):
41+
"""
42+
Implements the C callback function for loading modules from any location.
43+
44+
:arg c_str mod_name:
45+
The YANG module name
46+
:arg c_str mod_rev:
47+
The YANG module revision
48+
:arg c_str submod_name:
49+
The YANG submodule name
50+
:arg c_str submod_rev:
51+
The YANG submodule revision
52+
:arg user_data:
53+
The user data provided by user during registration. In this implementation
54+
it is always considered to be handle of Python object
55+
:arg fmt:
56+
The output pointer where to set the format of schema
57+
:arg module_data:
58+
The output pointer where to set the schema data itself
59+
:arg free_module_data:
60+
The output pointer of callback function which will be called when the schema
61+
data are no longer needed
62+
63+
:returns:
64+
The LY_SUCCESS in case the needed YANG (sub)module schema was found
65+
The LY_ENOT in case the needed YANG (sub)module schema was not found
66+
"""
67+
fmt[0] = lib.LYS_IN_UNKNOWN
68+
module_data[0] = ffi.NULL
69+
free_module_data[0] = lib.lypy_module_imp_data_free_clb
70+
instance = ffi.from_handle(user_data)
71+
ret = instance.get_module_data(
72+
c2str(mod_name), c2str(mod_rev), c2str(submod_name), c2str(submod_rev)
73+
)
74+
if ret is None:
75+
return lib.LY_ENOT
76+
in_fmt, content = ret
77+
fmt[0] = schema_in_format(in_fmt)
78+
module_data[0] = content
79+
return lib.LY_SUCCESS
80+
81+
82+
# -------------------------------------------------------------------------------------
83+
class ContextExternalModuleLoader:
84+
__slots__ = (
85+
"_cdata",
86+
"_module_data_clb",
87+
"_cffi_handle",
88+
"_cdata_modules",
89+
)
90+
91+
def __init__(self, cdata) -> None:
92+
self._cdata = cdata # C type: "struct ly_ctx *"
93+
self._module_data_clb = None
94+
self._cffi_handle = ffi.new_handle(self)
95+
self._cdata_modules = []
96+
97+
def free_module_data(self, cdata) -> None:
98+
"""
99+
Gets the YANG module schema data based requirements from libyang_c_module_imp_clb
100+
function and forward that request to user Python based callback function.
101+
The returned data from callback function are stored within the context to make sure
102+
of no memory access issues. These data a stored until the free_module_data function
103+
is called directly by libyang
104+
105+
:arg cdata:
106+
The pointer to YANG modelu schema (c_str), which shall be released from memory
107+
"""
108+
self._cdata_modules.remove(cdata)
109+
110+
def get_module_data(
111+
self,
112+
mod_name: Optional[str],
113+
mod_rev: Optional[str],
114+
submod_name: Optional[str],
115+
submod_rev: Optional[str],
116+
) -> Optional[Tuple[str, str]]:
117+
"""
118+
Gets the YANG module schema data based requirements from libyang_c_module_imp_clb
119+
function and forward that request to user Python based callback function.
120+
The returned data from callback function are stored within the context to make sure
121+
of no memory access issues. These data a stored until the free_module_data function
122+
is called directly by libyang
123+
124+
:arg self
125+
This instance on context
126+
:arg mod_name:
127+
The optional YANG module name
128+
:arg mod_rev:
129+
The optional YANG module revision
130+
:arg submod_name:
131+
The optional YANG submodule name
132+
:arg submod_rev:
133+
The optional YANG submodule revision
134+
135+
:returns:
136+
Tuple of format string and YANG (sub)module schema
137+
"""
138+
if self._module_data_clb is None:
139+
return "", None
140+
fmt_str, module_data = self._module_data_clb(
141+
mod_name, mod_rev, submod_name, submod_rev
142+
)
143+
if module_data is None:
144+
return fmt_str, None
145+
module_data_c = str2c(module_data)
146+
self._cdata_modules.append(module_data_c)
147+
return fmt_str, module_data_c
148+
149+
def set_module_data_clb(
150+
self,
151+
clb: Optional[
152+
Callable[
153+
[Optional[str], Optional[str], Optional[str], Optional[str]],
154+
Optional[Tuple[str, str]],
155+
]
156+
] = None,
157+
) -> None:
158+
"""
159+
Sets the callback function, which will be called if libyang context would like to
160+
load module or submodule, which is not locally available in context path(s).
161+
162+
:arg self
163+
This instance on context
164+
:arg clb:
165+
The callback function. The expected arguments are:
166+
mod_name: Module name
167+
mod_rev: Module revision
168+
submod_name: Submodule name
169+
submod_rev: Submodule revision
170+
The expeted return value is either:
171+
tuple of:
172+
format: The string format of the loaded data
173+
data: The YANG (sub)module data as string
174+
or None in case of error
175+
"""
176+
self._module_data_clb = clb
177+
if clb is None:
178+
lib.ly_ctx_set_module_imp_clb(self._cdata, ffi.NULL, ffi.NULL)
179+
else:
180+
lib.ly_ctx_set_module_imp_clb(
181+
self._cdata, lib.lypy_module_imp_clb, self._cffi_handle
182+
)
183+
184+
22185
# -------------------------------------------------------------------------------------
23186
class Context:
24-
__slots__ = ("cdata", "__dict__")
187+
__slots__ = (
188+
"cdata",
189+
"external_module_loader",
190+
"__dict__",
191+
)
25192

26193
def __init__(
27194
self,
@@ -36,6 +203,7 @@ def __init__(
36203
):
37204
if cdata is not None:
38205
self.cdata = ffi.cast("struct ly_ctx *", cdata)
206+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
39207
return # already initialized
40208

41209
options = 0
@@ -87,6 +255,7 @@ def __init__(
87255
)
88256
if not self.cdata:
89257
raise self.error("cannot create context")
258+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
90259

91260
def compile_schema(self):
92261
ret = lib.ly_ctx_compile(self.cdata)

tests/test_context.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,26 @@ def test_ctx_disable_searchdirs(self):
116116
with Context(YANG_DIR, disable_searchdirs=True) as ctx:
117117
with self.assertRaises(LibyangError):
118118
ctx.load_module("yolo-nodetypes")
119+
120+
def test_ctx_using_clb(self):
121+
def get_module_valid_clb(mod_name, *_):
122+
YOLO_NODETYPES_MOD_PATH = os.path.join(YANG_DIR, "yolo/yolo-nodetypes.yang")
123+
self.assertEqual(mod_name, "yolo-nodetypes")
124+
with open(YOLO_NODETYPES_MOD_PATH, encoding="utf-8") as f:
125+
mod_str = f.read()
126+
return "yang", mod_str
127+
128+
def get_module_invalid_clb(mod_name, *_):
129+
return None
130+
131+
with Context(YANG_DIR, disable_searchdirs=True) as ctx:
132+
with self.assertRaises(LibyangError):
133+
ctx.load_module("yolo-nodetypes")
134+
135+
ctx.external_module_loader.set_module_data_clb(get_module_invalid_clb)
136+
with self.assertRaises(LibyangError):
137+
mod = ctx.load_module("yolo-nodetypes")
138+
139+
ctx.external_module_loader.set_module_data_clb(get_module_valid_clb)
140+
mod = ctx.load_module("yolo-nodetypes")
141+
self.assertIsInstance(mod, Module)

0 commit comments

Comments
 (0)