Skip to content

Commit 159117c

Browse files
stewegsamuel-gauthier
authored andcommitted
data: add anydata and anyxml support to dict_to_dnode
dict_to_dnode() skips anydata and anyxml nodes. The schema lookup uses the default iter_children() types, which do not include them, so these keys are dropped as unknown elements. Look them up as well, and create them with lyd_new_any(). A dict value is dumped to JSON and a string value is passed as is. Since libyang v5, libyang parses this string into the node content, so there is no value type to give anymore. An empty dict creates an empty node. Passing "{}" to libyang is not safe: when the parsed tree is empty, libyang keeps our buffer as the node value, but python still owns and frees it. A JSON string is loaded first, so this also covers strings like "{ }". Note that the default iter_children() types stay the same, so iterating over a schema node still skips anydata and anyxml nodes. Closes: #115 Signed-off-by: Stefan Gula <steweg@gmail.com> Signed-off-by: Samuel Gauthier <samuel.gauthier@6wind.com>
1 parent 768825f commit 159117c

3 files changed

Lines changed: 107 additions & 1 deletion

File tree

‎cffi/cdefs.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,7 @@ LY_ERR lyd_new_term(struct lyd_node *, const struct lys_module *, const char *,
894894
char* lyd_path(const struct lyd_node *, LYD_PATH_TYPE, char *, size_t);
895895
LY_ERR lyd_new_inner(struct lyd_node *, const struct lys_module *, const char *, ly_bool, struct lyd_node **);
896896
LY_ERR lyd_new_list(struct lyd_node *, const struct lys_module *, const char *, uint32_t, struct lyd_node **node, ...);
897+
LY_ERR lyd_new_any(struct lyd_node *, const struct lys_module *, const char *, const struct lyd_node *, const char *, uint32_t, uint32_t, struct lyd_node **);
897898

898899
struct lyd_node_inner {
899900
union {

‎libyang/data.py‎

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
# Copyright (c) 2021 RACOM s.r.o.
33
# SPDX-License-Identifier: MIT
44

5+
import json
56
import logging
67
from typing import IO, Any, Dict, Iterator, Optional, Tuple, Union
78

89
from _libyang import ffi, lib
910
from .keyed_list import KeyedList
1011
from .schema import (
1112
Module,
13+
SAnydata,
14+
SAnyxml,
1215
SContainer,
1316
SLeaf,
1417
SLeafList,
@@ -1319,6 +1322,55 @@ def _create_list(_parent, module, name, key_values, in_rpc_output=False):
13191322
created.append(n[0])
13201323
return n[0]
13211324

1325+
def _create_any(_parent, module, name, value, in_rpc_output=False):
1326+
# libyang parses a JSON or XML string into the anydata/anyxml content
1327+
if isinstance(value, str) and value.lstrip().startswith("{"):
1328+
value = json.loads(value)
1329+
if isinstance(value, dict):
1330+
# libyang keeps our buffer as the node value when the parsed
1331+
# tree is empty, so create an empty node instead
1332+
value = json.dumps(value) if value else None
1333+
elif not isinstance(value, str):
1334+
raise TypeError("%s: python value is not a dict/str: %r" % (name, value))
1335+
1336+
n = ffi.new("struct lyd_node **")
1337+
flags = newval_flags(rpc_output=in_rpc_output, store_only=store_only)
1338+
ret = lib.lyd_new_any(
1339+
_parent,
1340+
module.cdata,
1341+
str2c(name),
1342+
ffi.NULL,
1343+
str2c(value),
1344+
0,
1345+
flags,
1346+
n,
1347+
)
1348+
if ret != lib.LY_SUCCESS:
1349+
if _parent:
1350+
parent_path = repr(DNode.new(module.context, _parent).path())
1351+
else:
1352+
parent_path = "module %r" % module.name()
1353+
raise module.context.error(
1354+
"failed to create anydata/anyxml %r as a child of %s",
1355+
name,
1356+
parent_path,
1357+
)
1358+
created.append(n[0])
1359+
1360+
# same as the iter_children() defaults, plus anydata and anyxml
1361+
schema_types = (
1362+
SNode.ACTION,
1363+
SNode.CONTAINER,
1364+
SNode.LIST,
1365+
SNode.RPC,
1366+
SNode.LEAF,
1367+
SNode.LEAFLIST,
1368+
SNode.NOTIF,
1369+
SNode.CHOICE,
1370+
SNode.CASE,
1371+
SNode.ANYDATA,
1372+
SNode.ANYXML,
1373+
)
13221374
schema_cache = {}
13231375

13241376
def _find_schema(schema_parent, name, prefix):
@@ -1335,7 +1387,7 @@ def _find_schema(schema_parent, name, prefix):
13351387
if schema_parent is None:
13361388
# there may not be any input or any output node in the rpc
13371389
return None, None
1338-
for s in schema_parent:
1390+
for s in schema_parent.children(types=schema_types):
13391391
if s.name() != name:
13401392
continue
13411393
mod = s.module()
@@ -1435,6 +1487,9 @@ def _to_dnode(_dic, _schema, _parent=ffi.NULL, in_rpc_output=False):
14351487
n = _create_container(_parent, module, name, in_rpc_output)
14361488
_to_dnode(value, s, n, in_rpc_output)
14371489

1490+
elif isinstance(s, (SAnydata, SAnyxml)):
1491+
_create_any(_parent, module, name, value, in_rpc_output)
1492+
14381493
result = None
14391494

14401495
try:

‎tests/test_data.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from _libyang import lib
1212
from libyang import (
1313
Context,
14+
DAnydata,
1415
DAnyxml,
1516
DataType,
1617
DContainer,
@@ -1159,3 +1160,52 @@ def test_dnode_parse_json_null(self):
11591160
dnode = self.ctx.parse_data_mem(JSON, "json", json_null=True)
11601161
dnode_names = [d.name() for d in dnode.siblings()]
11611162
self.assertFalse("ip-address" in dnode_names)
1163+
1164+
def test_dict_to_dnode_anydata(self):
1165+
module = self.ctx.load_module("yolo-nodetypes")
1166+
dic = {"yolo-nodetypes:any1": {"key1": "val1"}}
1167+
dnode = dict_to_dnode(dic, module, validate=False)
1168+
try:
1169+
self.assertIsInstance(dnode, DAnydata)
1170+
self.assertEqual(json.loads(dnode.value("json")), {"key1": "val1"})
1171+
finally:
1172+
dnode.free()
1173+
1174+
def test_dict_to_dnode_anydata_empty(self):
1175+
module = self.ctx.load_module("yolo-nodetypes")
1176+
for value in ({}, "{ }"):
1177+
dnode = dict_to_dnode(
1178+
{"yolo-nodetypes:any1": value}, module, validate=False
1179+
)
1180+
try:
1181+
self.assertIsInstance(dnode, DAnydata)
1182+
self.assertEqual(dnode.value("json"), None)
1183+
finally:
1184+
dnode.free()
1185+
1186+
def test_dict_to_dnode_anydata_rpc(self):
1187+
module = self.ctx.get_module("yolo-system")
1188+
dic = {"format-disk": {"disk": "sda", "json-info": {"key1": "val1"}}}
1189+
dnode = dict_to_dnode(dic, module, rpc=True)
1190+
try:
1191+
anydata = dnode.find_path("/yolo-system:format-disk/json-info")
1192+
self.assertIsInstance(anydata, DAnydata)
1193+
self.assertEqual(json.loads(anydata.value("json")), {"key1": "val1"})
1194+
finally:
1195+
dnode.free()
1196+
1197+
def test_dict_to_dnode_anyxml(self):
1198+
module = self.ctx.get_module("yolo-system")
1199+
dic = {"format-disk": {"html-info": "<p>text</p>"}}
1200+
dnode = dict_to_dnode(dic, module, rpc=True)
1201+
try:
1202+
anyxml = dnode.find_path("/yolo-system:format-disk/html-info")
1203+
self.assertIsInstance(anyxml, DAnyxml)
1204+
self.assertEqual(anyxml.value("xml").strip(), "<p>text</p>")
1205+
finally:
1206+
dnode.free()
1207+
1208+
def test_dict_to_dnode_anydata_bad_type(self):
1209+
module = self.ctx.load_module("yolo-nodetypes")
1210+
with self.assertRaises(TypeError):
1211+
dict_to_dnode({"yolo-nodetypes:any1": 42}, module, validate=False)

0 commit comments

Comments
 (0)