Skip to content

Commit ebf1f1a

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 f14116c commit ebf1f1a

File tree

3 files changed

+200
-2
lines changed

3 files changed

+200
-2
lines changed

cffi/cdefs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,11 @@ typedef enum {
10711071
LY_ERR lys_parse(struct ly_ctx *, struct ly_in *, LYS_INFORMAT, const char **, struct lys_module **);
10721072
LY_ERR ly_ctx_new_ylpath(const char *, const char *, LYD_FORMAT, int, struct ly_ctx **);
10731073
LY_ERR ly_ctx_get_yanglib_data(const struct ly_ctx *, struct lyd_node **, const char *, ...);
1074+
typedef void (*ly_module_imp_data_free_clb)(void *, void *);
1075+
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 *);
1076+
void ly_ctx_set_module_imp_clb(struct ly_ctx *, ly_module_imp_clb, void *);
1077+
extern "Python" void lypy_module_imp_data_free_clb(void *, void *);
1078+
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 *);
10741079

10751080
LY_ERR lydict_insert(const struct ly_ctx *, const char *, size_t, const char **);
10761081
LY_ERR lydict_remove(const struct ly_ctx *, const char *);

libyang/context.py

Lines changed: 172 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,177 @@
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+
ret = self._module_data_clb(
141+
mod_name, mod_rev, submod_name, submod_rev
142+
)
143+
if ret is None:
144+
return None
145+
fmt_str, module_data = ret
146+
module_data_c = str2c(module_data)
147+
self._cdata_modules.append(module_data_c)
148+
return fmt_str, module_data_c
149+
150+
def set_module_data_clb(
151+
self,
152+
clb: Optional[
153+
Callable[
154+
[Optional[str], Optional[str], Optional[str], Optional[str]],
155+
Optional[Tuple[str, str]],
156+
]
157+
] = None,
158+
) -> None:
159+
"""
160+
Sets the callback function, which will be called if libyang context would like to
161+
load module or submodule, which is not locally available in context path(s).
162+
163+
:arg self
164+
This instance on context
165+
:arg clb:
166+
The callback function. The expected arguments are:
167+
mod_name: Module name
168+
mod_rev: Module revision
169+
submod_name: Submodule name
170+
submod_rev: Submodule revision
171+
The expeted return value is either:
172+
tuple of:
173+
format: The string format of the loaded data
174+
data: The YANG (sub)module data as string
175+
or None in case of error
176+
"""
177+
self._module_data_clb = clb
178+
if clb is None:
179+
lib.ly_ctx_set_module_imp_clb(self._cdata, ffi.NULL, ffi.NULL)
180+
else:
181+
lib.ly_ctx_set_module_imp_clb(
182+
self._cdata, lib.lypy_module_imp_clb, self._cffi_handle
183+
)
184+
185+
22186
# -------------------------------------------------------------------------------------
23187
class Context:
24-
__slots__ = ("cdata", "__dict__")
188+
__slots__ = (
189+
"cdata",
190+
"external_module_loader",
191+
"__dict__",
192+
)
25193

26194
def __init__(
27195
self,
@@ -37,6 +205,7 @@ def __init__(
37205
):
38206
if cdata is not None:
39207
self.cdata = ffi.cast("struct ly_ctx *", cdata)
208+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
40209
return # already initialized
41210

42211
options = 0
@@ -90,6 +259,7 @@ def __init__(
90259
)
91260
if not self.cdata:
92261
raise self.error("cannot create context")
262+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
93263

94264
def compile_schema(self):
95265
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
@@ -124,3 +124,26 @@ def test_ctx_disable_searchdirs(self):
124124
with Context(YANG_DIR, disable_searchdirs=True) as ctx:
125125
with self.assertRaises(LibyangError):
126126
ctx.load_module("yolo-nodetypes")
127+
128+
def test_ctx_using_clb(self):
129+
def get_module_valid_clb(mod_name, *_):
130+
YOLO_NODETYPES_MOD_PATH = os.path.join(YANG_DIR, "yolo/yolo-nodetypes.yang")
131+
self.assertEqual(mod_name, "yolo-nodetypes")
132+
with open(YOLO_NODETYPES_MOD_PATH, encoding="utf-8") as f:
133+
mod_str = f.read()
134+
return "yang", mod_str
135+
136+
def get_module_invalid_clb(mod_name, *_):
137+
return None
138+
139+
with Context(YANG_DIR, disable_searchdirs=True) as ctx:
140+
with self.assertRaises(LibyangError):
141+
ctx.load_module("yolo-nodetypes")
142+
143+
ctx.external_module_loader.set_module_data_clb(get_module_invalid_clb)
144+
with self.assertRaises(LibyangError):
145+
mod = ctx.load_module("yolo-nodetypes")
146+
147+
ctx.external_module_loader.set_module_data_clb(get_module_valid_clb)
148+
mod = ctx.load_module("yolo-nodetypes")
149+
self.assertIsInstance(mod, Module)

0 commit comments

Comments
 (0)