Skip to content

Commit 4831b70

Browse files
stewegsamuel-gauthier
authored andcommitted
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. Closes: #103 Signed-off-by: Stefan Gula <steweg@gmail.com> Signed-off-by: Samuel Gauthier <samuel.gauthier@6wind.com>
1 parent f14116c commit 4831b70

File tree

3 files changed

+196
-2
lines changed

3 files changed

+196
-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: 168 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,173 @@
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+
Implement 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+
Free previously stored data, obtained after a get_module_data.
100+
101+
:arg cdata:
102+
The pointer to YANG modelu schema (c_str), which shall be released from memory
103+
"""
104+
self._cdata_modules.remove(cdata)
105+
106+
def get_module_data(
107+
self,
108+
mod_name: Optional[str],
109+
mod_rev: Optional[str],
110+
submod_name: Optional[str],
111+
submod_rev: Optional[str],
112+
) -> Optional[Tuple[str, str]]:
113+
"""
114+
Get the YANG module schema data based requirements from libyang_c_module_imp_clb
115+
function and forward that request to user Python based callback function.
116+
117+
The returned data from callback function are stored within the context to make sure
118+
of no memory access issues. These data a stored until the free_module_data function
119+
is called directly by libyang.
120+
121+
:arg self
122+
This instance on context
123+
:arg mod_name:
124+
The optional YANG module name
125+
:arg mod_rev:
126+
The optional YANG module revision
127+
:arg submod_name:
128+
The optional YANG submodule name
129+
:arg submod_rev:
130+
The optional YANG submodule revision
131+
132+
:returns:
133+
Tuple of format string and YANG (sub)module schema
134+
"""
135+
if self._module_data_clb is None:
136+
return "", None
137+
fmt_str, module_data = self._module_data_clb(
138+
mod_name, mod_rev, submod_name, submod_rev
139+
)
140+
if module_data is None:
141+
return fmt_str, None
142+
module_data_c = str2c(module_data)
143+
self._cdata_modules.append(module_data_c)
144+
return fmt_str, module_data_c
145+
146+
def set_module_data_clb(
147+
self,
148+
clb: Optional[
149+
Callable[
150+
[Optional[str], Optional[str], Optional[str], Optional[str]],
151+
Optional[Tuple[str, str]],
152+
]
153+
] = None,
154+
) -> None:
155+
"""
156+
Set the callback function, which will be called if libyang context would like to
157+
load module or submodule, which is not locally available in context path(s).
158+
159+
:arg self
160+
This instance on context
161+
:arg clb:
162+
The callback function. The expected arguments are:
163+
mod_name: Module name
164+
mod_rev: Module revision
165+
submod_name: Submodule name
166+
submod_rev: Submodule revision
167+
The expeted return value is either:
168+
tuple of:
169+
format: The string format of the loaded data
170+
data: The YANG (sub)module data as string
171+
or None in case of error
172+
"""
173+
self._module_data_clb = clb
174+
if clb is None:
175+
lib.ly_ctx_set_module_imp_clb(self._cdata, ffi.NULL, ffi.NULL)
176+
else:
177+
lib.ly_ctx_set_module_imp_clb(
178+
self._cdata, lib.lypy_module_imp_clb, self._cffi_handle
179+
)
180+
181+
22182
# -------------------------------------------------------------------------------------
23183
class Context:
24-
__slots__ = ("cdata", "__dict__")
184+
__slots__ = (
185+
"cdata",
186+
"external_module_loader",
187+
"__dict__",
188+
)
25189

26190
def __init__(
27191
self,
@@ -37,6 +201,7 @@ def __init__(
37201
):
38202
if cdata is not None:
39203
self.cdata = ffi.cast("struct ly_ctx *", cdata)
204+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
40205
return # already initialized
41206

42207
options = 0
@@ -90,6 +255,7 @@ def __init__(
90255
)
91256
if not self.cdata:
92257
raise self.error("cannot create context")
258+
self.external_module_loader = ContextExternalModuleLoader(self.cdata)
93259

94260
def compile_schema(self):
95261
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)