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)