diff --git a/Makefile b/Makefile
index 722982c..be97f82 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
PROJECT = yangson
-VERSION = 1.3.24
+VERSION = 1.3.26
.PHONY = tags deps install-deps test
tags:
diff --git a/docs/examples/ex2/example-data.json b/docs/examples/ex2/example-data.json
index d18cad4..4dd08dd 100644
--- a/docs/examples/ex2/example-data.json
+++ b/docs/examples/ex2/example-data.json
@@ -8,6 +8,14 @@
{
"number": 3,
"in-words": "three"
+ },
+ {
+ "number": 7,
+ "in-words": "seven"
+ },
+ {
+ "number": 8,
+ "in-words": "eight"
}
],
"bar": true
diff --git a/docs/examples/ex2/test.py b/docs/examples/ex2/test.py
deleted file mode 100644
index d48f110c..0000000
--- a/docs/examples/ex2/test.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import json
-from yangson import DataModel
-
-dm = DataModel.from_file('yang-library-ex2.json')
-with open('example-data.json') as infile:
- ri = json.load(infile)
-inst = dm.from_raw(ri)
-iwd = inst.add_defaults()
-baz = iwd["example-2:bag"]["baz"]
-print(baz)
diff --git a/docs/instance.rst b/docs/instance.rst
index 74416ef..3475e81 100644
--- a/docs/instance.rst
+++ b/docs/instance.rst
@@ -219,13 +219,13 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
>>> foo6 = foo[0]
>>> foo6.value['number']
6
- >>> foo3 = foo[-1]
- >>> foo3.value['in-words']
- 'three'
- >>> foo[2]
+ >>> fool = foo[-1]
+ >>> fool.value['in-words']
+ 'eight'
+ >>> foo[4]
Traceback (most recent call last):
...
- yangson.instance.NonexistentInstance: [/example-2:bag/foo] entry 2
+ yangson.instance.NonexistentInstance: [/example-2:bag/foo] entry 4
.. method:: __iter__()
@@ -246,7 +246,7 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
.. doctest::
>>> [e.json_pointer() for e in foo]
- ['/example-2:bag/foo/0', '/example-2:bag/foo/1']
+ ['/example-2:bag/foo/0', '/example-2:bag/foo/1', '/example-2:bag/foo/2', '/example-2:bag/foo/3']
An attempt to iterate over an :class:`InstanceNode` that has a
scalar value raises :exc:`~.InstanceValueError`.
@@ -309,9 +309,9 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
['bar', 'baz', 'foo']
>>> xfoo = foo.delete_item(0)
>>> len(xfoo.value)
- 1
+ 3
>>> len(foo.value) # foo is unchanged
- 2
+ 4
.. method:: look_up(**keys: Dict[InstanceName, ScalarValue]) -> ArrayEntry
@@ -329,9 +329,9 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
.. doctest::
- >>> foo3 = foo.look_up(number=3)
- >>> foo3.json_pointer()
- '/example-2:bag/foo/1'
+ >>> foo8 = foo.look_up(number=8)
+ >>> foo8.json_pointer()
+ '/example-2:bag/foo/3'
.. method:: up() -> InstanceNode
@@ -376,9 +376,15 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
flag is set, the :meth:`update` method “cooks” the raw value
first into the Python's :class:`decimal.Decimal` type.
- >>> e3baz = e2bag['baz'].update_from_raw('2.7182818')
- >>> e3baz.value
- Decimal('2.7182818')
+ .. doctest::
+
+ >>> e3baz = e2bag['baz'].update('2.7182818', raw=True)
+ >>> e3baz.value
+ Decimal('2.7182818')
+ >>> e2bag['foo'][0]['in-words'].update(66, raw=True)
+ Traceback (most recent call last):
+ ...
+ yangson.exceptions.RawTypeError: [/example-2:bag/foo/0/in-words] expected string value
.. method:: goto(iroute: InstanceRoute) -> InstanceNode
@@ -607,8 +613,8 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
.. doctest::
- >>> foo3.previous().json_pointer()
- '/example-2:bag/foo/0'
+ >>> foo8.previous().json_pointer()
+ '/example-2:bag/foo/2'
>>> foo6.previous()
Traceback (most recent call last):
...
@@ -626,10 +632,10 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
>>> foo6.next().json_pointer()
'/example-2:bag/foo/1'
- >>> foo3.next()
+ >>> foo8.next()
Traceback (most recent call last):
...
- yangson.instance.NonexistentInstance: [/example-2:bag/foo/1] next of last
+ yangson.instance.NonexistentInstance: [/example-2:bag/foo/3] next of last
.. method:: insert_before(value: Union[RawValue, Value], raw: bool \
= False) -> ArrayEntry
@@ -641,9 +647,9 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
.. doctest::
- >>> foo4 = foo3.insert_before({'number': 4, 'in-words': 'four'}, raw=True)
+ >>> foo4 = foo8.insert_before({'number': 4, 'in-words': 'four'}, raw=True)
>>> [en['number'] for en in foo4.up().value]
- [6, 4, 3]
+ [6, 3, 7, 4, 8]
.. method:: insert_after(value: Union[RawValue, Value], raw: bool \
= False) -> ArrayEntry
@@ -657,7 +663,7 @@ __ http://www.sphinx-doc.org/en/stable/ext/doctest.html
>>> foo5 = foo4.insert_after({'number': 5, 'in-words': 'five'}, raw=True)
>>> [en['number'] for en in foo5.up().value]
- [6, 4, 5, 3]
+ [6, 3, 7, 4, 5, 8]
.. autoclass:: InstanceRoute
:show-inheritance:
diff --git a/yang-modules/ietf/ietf-yang-metadata@2016-08-05.yang b/yang-modules/ietf/ietf-yang-metadata@2016-08-05.yang
new file mode 100644
index 0000000..f59426a
--- /dev/null
+++ b/yang-modules/ietf/ietf-yang-metadata@2016-08-05.yang
@@ -0,0 +1,80 @@
+module ietf-yang-metadata {
+ namespace "urn:ietf:params:xml:ns:yang:ietf-yang-metadata";
+ prefix md;
+
+ organization
+ "IETF NETMOD (NETCONF Data Modeling Language) Working Group";
+ contact
+ "WG Web:
+
+ WG List:
+
+ WG Chair: Lou Berger
+
+
+ WG Chair: Kent Watsen
+
+
+ Editor: Ladislav Lhotka
+ ";
+ description
+ "This YANG module defines an 'extension' statement that allows
+ for defining metadata annotations.
+
+ Copyright (c) 2016 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject to
+ the license terms contained in, the Simplified BSD License set
+ forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (http://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC 7952
+ (http://www.rfc-editor.org/info/rfc7952); see the RFC itself
+ for full legal notices.";
+
+ revision 2016-08-05 {
+ description
+ "Initial revision.";
+ reference
+ "RFC 7952: Defining and Using Metadata with YANG";
+ }
+
+ extension annotation {
+ argument name;
+ description
+ "This extension allows for defining metadata annotations in
+ YANG modules. The 'md:annotation' statement can appear only
+ at the top level of a YANG module or submodule, i.e., it
+ becomes a new alternative in the ABNF production rule for
+ 'body-stmts' (Section 14 in RFC 7950).
+
+ The argument of the 'md:annotation' statement defines the name
+ of the annotation. Syntactically, it is a YANG identifier as
+ defined in Section 6.2 of RFC 7950.
+
+ An annotation defined with this 'extension' statement inherits
+ the namespace and other context from the YANG module in which
+ it is defined.
+
+ The data type of the annotation value is specified in the same
+ way as for a leaf data node using the 'type' statement.
+
+ The semantics of the annotation and other documentation can be
+ specified using the following standard YANG substatements (all
+ are optional): 'description', 'if-feature', 'reference',
+ 'status', and 'units'.
+
+ A server announces support for a particular annotation by
+ including the module in which the annotation is defined among
+ the advertised YANG modules, e.g., in a NETCONF
+ message or in the YANG library (RFC 7950). The annotation can
+ then be attached to any instance of a data node defined in any
+ YANG module that is advertised by the server.
+
+ XML encoding and JSON encoding of annotations are defined in
+ RFC 7952.";
+ }
+}
diff --git a/yang-modules/test_metadata/meta@2016-04-26.yang b/yang-modules/test_metadata/meta@2016-04-26.yang
new file mode 100644
index 0000000..e4f5779
--- /dev/null
+++ b/yang-modules/test_metadata/meta@2016-04-26.yang
@@ -0,0 +1,25 @@
+module meta {
+
+ yang-version "1.1";
+
+ namespace "http://example.com/meta";
+
+ prefix "m";
+
+ import ietf-yang-types {
+ prefix "yang";
+ revision-date 2013-07-15;
+ }
+ import ietf-yang-metadata {
+ prefix "md";
+ }
+
+ revision 2016-04-26;
+
+ md:annotation last-modified {
+ type yang:date-and-time;
+ description
+ "This annotation contains the date and time when the
+ annotated instance was last modified (or created).";
+ }
+}
diff --git a/yang-modules/test_metadata/other@2016-04-26.yang b/yang-modules/test_metadata/other@2016-04-26.yang
new file mode 100644
index 0000000..f96b408
--- /dev/null
+++ b/yang-modules/test_metadata/other@2016-04-26.yang
@@ -0,0 +1,24 @@
+module other {
+
+ yang-version "1.1";
+
+ namespace "http://example.com/other";
+
+ prefix "m";
+
+ import ietf-yang-types {
+ prefix "yang";
+ revision-date 2013-07-15;
+ }
+
+ revision 2016-04-26;
+
+ container contA {
+ leaf leafA {
+ type int32;
+ }
+ leaf leafB {
+ type string;
+ }
+ }
+}
diff --git a/yang-modules/test_metadata/yang-library.json b/yang-modules/test_metadata/yang-library.json
new file mode 100644
index 0000000..a0ae04c
--- /dev/null
+++ b/yang-modules/test_metadata/yang-library.json
@@ -0,0 +1,33 @@
+{
+ "ietf-yang-library:modules-state": {
+ "module-set-id": "b6d7e0614440c5ad8a7370fe46c777254d331983",
+ "module": [
+ {
+ "name": "meta",
+ "revision": "2016-04-26",
+ "schema": "https://example.com/meta.yang",
+ "namespace": "http://example.com/meta",
+ "conformance-type": "implement"
+ },
+ {
+ "name": "other",
+ "revision": "2016-04-26",
+ "schema": "https://example.com/other.yang",
+ "namespace": "http://example.com/other",
+ "conformance-type": "implement"
+ },
+ {
+ "name": "ietf-yang-types",
+ "revision": "2013-07-15",
+ "namespace": "urn:ietf:params:xml:ns:yang:ietf-yang-types",
+ "conformance-type": "import"
+ },
+ {
+ "name": "ietf-yang-metadata",
+ "revision": "2016-08-05",
+ "namespace": "urn:ietf:params:xml:ns:yang:ietf-yang-metadata",
+ "conformance-type": "import"
+ }
+ ]
+ }
+}
diff --git a/yangson/instance.py b/yangson/instance.py
index 0657086..651187f 100644
--- a/yangson/instance.py
+++ b/yangson/instance.py
@@ -53,14 +53,14 @@ class LinkedList:
"""Persistent linked list of instance values."""
@classmethod
- def from_list(cls, vals: List[Value] = []) -> "LinkedList":
+ def from_list(cls, vals: List[Value] = [], reverse: bool = False) -> "LinkedList":
"""Create an instance from a standard list.
Args:
vals: Python list of instance values.
"""
res = EmptyList()
- for v in vals[::-1]:
+ for v in (vals if reverse else vals[::-1]):
res = cls(v, res)
return res
@@ -266,7 +266,8 @@ def update(self, value: Union[RawValue, Value],
Returns:
Copy of the receiver with the updated value.
"""
- newval = self.schema_node.from_raw(value, self.json_pointer()) if raw else value
+ newval = self.schema_node.from_raw(
+ value, self.json_pointer()) if raw else value
return self._copy(newval)
def goto(self, iroute: "InstanceRoute") -> "InstanceNode":
@@ -359,7 +360,7 @@ def _entry(self, index: int) -> "ArrayEntry":
val = self.value
try:
i = len(val) + index if index < 0 else index
- return ArrayEntry(i, LinkedList.from_list(val[:i]),
+ return ArrayEntry(i, LinkedList.from_list(val[:i], reverse=True),
LinkedList.from_list(val[i + 1:]),
val[index], self, self.schema_node,
val.timestamp)
@@ -378,6 +379,8 @@ def _peek_schema_route(self, sroute: SchemaRoute) -> Value:
return self.peek(irt)
def _member_schema_node(self, name: InstanceName) -> "DataNode":
+ if name.startswith("@"):
+ return self.schema_node.schema_root()
qname = self.schema_node._iname2qname(name)
res = self.schema_node.get_data_child(*qname)
if res is None:
diff --git a/yangson/parser.py b/yangson/parser.py
index 61d0edb..15521f6 100644
--- a/yangson/parser.py
+++ b/yangson/parser.py
@@ -39,7 +39,7 @@ class Parser:
# Regular expressions
- ident_re = re.compile("[a-zA-Z_][a-zA-Z0-9_.-]*")
+ ident_re = re.compile(r"(@?[a-zA-Z_][a-zA-Z0-9_.-]*|@)")
"""Regular expression for YANG identifier."""
ws_re = re.compile(r"[ \n\t\r]*")
diff --git a/yangson/schemanode.py b/yangson/schemanode.py
index de98db7..8c6dfa8 100644
--- a/yangson/schemanode.py
+++ b/yangson/schemanode.py
@@ -39,6 +39,7 @@
* AnyContentNode: Abstract superclass for anydata and anyxml nodes..
* AnydataNode: YANG anydata node.
* AnyxmlNode: YANG anyxml node.
+* AnnotationNode: YANG extension RFC 7952 annotation node.
"""
from datetime import datetime
@@ -285,6 +286,7 @@ def _tree_line_prefix(self) -> str:
"identity": "_identity_stmt",
"ietf-netconf-acm:default-deny-all": "_nacm_default_deny_stmt",
"ietf-netconf-acm:default-deny-write": "_nacm_default_deny_stmt",
+ "ietf-yang-metadata:annotation": "_metadata_annotation_stmt",
"input": "_input_stmt",
"key": "_key_stmt",
"leaf": "_leaf_stmt",
@@ -358,6 +360,8 @@ def get_data_child(self, name: YangIdentifier,
"""Return data node directly under the receiver."""
ns = ns if ns else self.ns
todo = []
+ if ns is not None and ns.startswith("@"):
+ return self.schema_root().annotations
for child in self.children:
if child.name == name and child.ns == ns:
if isinstance(child, DataNode):
@@ -398,12 +402,23 @@ def from_raw(self, rval: RawObject, jptr: JSONPointer="") -> ObjectValue:
raise RawTypeError(jptr, "object")
res = ObjectValue()
for qn in rval:
- cn = self._iname2qname(qn)
- ch = self.get_data_child(*cn)
npath = jptr + "/" + qn
- if ch is None:
- raise RawMemberError(npath)
- res[ch.iname()] = ch.from_raw(rval[qn], npath)
+ if qn.startswith("@"):
+ if qn != "@":
+ cn = self._iname2qname(qn[1:])
+ ch = self.get_data_child(*cn)
+ if ch is None:
+ raise RawMemberError(npath)
+ res[qn] = self.schema_root().from_raw(rval[qn], npath)
+ else:
+ cn = self._iname2qname(qn)
+ ch = self.get_data_child(*cn)
+ if ch is None:
+ raise RawMemberError(npath)
+ if "@" in jptr and not isinstance(ch, AnnotationNode):
+ raise RawMemberError(npath)
+ iname = ch.iname()
+ res[iname] = ch.from_raw(rval[qn], npath)
return res
def _node_digest(self) -> Dict[str, Any]:
@@ -438,6 +453,8 @@ def _check_schema_pattern(self, inst: "InstanceNode",
p = self.schema_pattern
p._eval_when(inst)
for m in inst.value:
+ if m.startswith("@"):
+ continue
p = p.deriv(m, ctype)
if isinstance(p, NotAllowed):
raise SchemaError(inst.json_pointer(), "member-not-allowed", m + (
@@ -635,10 +652,21 @@ def _flatten(self) -> List[SchemaNode]:
class SchemaTreeNode(GroupNode):
"""Root node of a schema tree."""
+ def __init__(self):
+ """Initialize the class instance."""
+ super().__init__()
+
def data_parent(self) -> InternalNode:
"""Override the superclass method."""
return self.parent
+ def _metadata_annotation_stmt(self, stmt: Statement, sctx: SchemaContext):
+ """Handle annotation statement."""
+ node = AnnotationNode()
+ node.type = DataType._resolve_type(
+ stmt.find1("type", required=True), sctx)
+ self._handle_child(node, stmt, sctx)
+
class DataNode(SchemaNode):
"""Abstract superclass for all data nodes."""
@@ -754,7 +782,7 @@ def from_raw(self, rval: RawScalar, jptr: JSONPointer="") -> ScalarValue:
"""Override the superclass method."""
res = self.type.from_raw(rval)
if res is None:
- raise RawTypeError(jptr, self.type.name + " value")
+ raise RawTypeError(jptr, self.type.yang_type() + " value")
return res
def _node_digest(self) -> Dict[str, Any]:
@@ -1336,6 +1364,27 @@ def _tree_line_prefix(self) -> str:
return super()._tree_line_prefix() + "-n"
+class AnnotationNode(DataNode, TerminalNode):
+ """Annotation node."""
+
+ def __init__(self):
+ """Initialize the class instance."""
+ super().__init__()
+
+ @property
+ def mandatory(self) -> bool:
+ """Is the receiver a mandatory node?"""
+ return False
+
+ @property
+ def default(self) -> Optional[ScalarValue]:
+ """Default value of the receiver, if any."""
+ return None
+
+ def _tree_line_prefix(self) -> str:
+ return super()._tree_line_prefix() + " @"
+
+
from .xpathast import Expr, LocationPath, Step, Root # NOQA
from .instance import (ArrayEntry, EmptyList, InstanceNode, # NOQA
InstanceRoute, MemberName, ObjectMember)