From c6ebdc5934f362c074e2868b662aba8193c02b6a Mon Sep 17 00:00:00 2001 From: Ladislav Lhotka <lhotka@nic.cz> Date: Thu, 27 Oct 2016 09:58:00 +0200 Subject: [PATCH] Reimplement array entries using persistent linked list. --- docs/instvalue.rst | 5 +- yangson/instance.py | 236 ++++++++++++++++++++++++++++--------------- yangson/instvalue.py | 17 ++-- yangson/nodeset.py | 4 +- yangson/xpathast.py | 2 +- 5 files changed, 170 insertions(+), 94 deletions(-) diff --git a/docs/instvalue.rst b/docs/instvalue.rst index f0075b6..db0a0f3 100644 --- a/docs/instvalue.rst +++ b/docs/instvalue.rst @@ -63,7 +63,10 @@ data structures with additional attributs and methods: Return a shallow copy of the receiver with :attr:`last_modified` set to current time. - .. automethod:: stamp + .. method:: __setitem__(self, key: InstKey, value: Value) -> None + + Set an array entry or object member *key* to *value* and update + receiver's timestamp to the current time. .. method:: __eq__(val: StructuredValue) -> bool diff --git a/yangson/instance.py b/yangson/instance.py index 5076369..9ee3af9 100644 --- a/yangson/instance.py +++ b/yangson/instance.py @@ -19,6 +19,7 @@ This module implements the following classes: +* LinkedList: Persistent linked list of instance values. * InstanceNode: Abstract class for instance nodes. * RootNode: Root of the data tree. * ObjectMember: Instance node that is an object member. @@ -37,7 +38,7 @@ The module defines the following exceptions: """ from datetime import datetime -from typing import Any, Callable, List, Tuple +from typing import Any, Callable, List, Tuple, Union from urllib.parse import unquote from .exceptions import YangsonException from .context import Context @@ -45,13 +46,84 @@ from .enumerations import ContentType from .instvalue import * from .parser import EndOfInput, Parser, UnexpectedInput from .typealiases import * +from .typealiases import _Singleton + +class LinkedList: + """Persistent linked list of instance values.""" + + @classmethod + def from_list(cls, vals: List[Value] = []) -> "LinkedList": + """Create an instance from a standard list. + + Args: + vals: Python list of instance values. + """ + res = _EmptyList() + for v in vals[::-1]: + res = res.cons(v) + return res + + def __init__(self, head: Value, tail: "LinkedList"): + """Initialize the class instance.""" + self.head = head + """Head of the linked list.""" + self.tail = tail + """Tail of the linked list.""" + + def __bool__(self): + """Return receiver's boolean value.""" + return True + + def __iter__(self): + """Iterate the receiver's entries.""" + l = self + while True: + try: + n, l = l.pop() + except IndexError: + raise StopIteration from None + yield n + + def cons(self, val: Value) -> "LinkedList": + """Prepend a value to the receiver in the persistent way. + + Args: + val: Instance value. + + Returns: A new linked list. + """ + return LinkedList(val, self) + + def pop(self) -> Tuple[Value, "LinkedList"]: + """Deconstruct the receiver. + + Returns: A tuple with receiver's head and tail, respectively. + """ + return (self.head, self.tail) + +class _EmptyList(LinkedList, metaclass=_Singleton): + """Singleton class representing the empty linked list.""" + + def __init__(self): + pass + + def __bool__(self): + return False + + def __getitem__(self, key): + raise IndexError + + def pop(self) -> None: + raise IndexError class InstanceNode: """YANG data node instance implemented as a zipper structure.""" - def __init__(self, value: Value, parinst: Optional["InstanceNode"], - schema_node: "DataNode", timestamp: datetime): + def __init__(self, key: InstKey, value: Value, parinst: "InstanceNode", + schema_node: "DataNode", timestamp: datetime): """Initialize the class instance.""" + self.path = parinst.path + (key,) # type: List[InstKey] + """Path in the data tree.""" self.parinst = parinst # type: Optional["InstanceNode"] """Parent instance node, or ``None`` for the root node.""" self.schema_node = schema_node # type: DataNode @@ -66,11 +138,6 @@ class InstanceNode: """The receiver's namespace.""" return self.schema_node.ns - @property - def qual_name(self) -> Optional[QualName]: - """The receiver's qualified name.""" - return None - def __str__(self) -> str: """Return string representation of the receiver's value.""" sn = self.schema_node @@ -83,12 +150,7 @@ class InstanceNode: def json_pointer(self) -> str: """Return JSON Pointer [RFC6901]_ of the receiver.""" - parents = [] - inst = self - while inst.parinst: - parents.append(inst) - inst = inst.parinst - return "/" + "/".join([i._pointer_fragment() for i in parents[::-1]]) + return "/" + "/".join([str(c) for c in self.path]) def member(self, name: InstanceName) -> "ObjectMember": """Return an instance node corresponding to a receiver's member. @@ -134,8 +196,7 @@ class InstanceNode: csn = self._member_schema_node(name) newval = self.value.copy() newval[name] = csn.from_raw(value) if raw else value - ts = datetime.now() - return self._copy(ObjectValue(newval, ts) , ts).member(name) + return self._copy(newval).member(name) def delete_member(self, name: InstanceName) -> "InstanceNode": """Return a copy of the receiver with a member deleted from its value. @@ -155,8 +216,7 @@ class InstanceNode: del newval[name] except KeyError: raise NonexistentInstance(self, "member " + name) from None - ts = datetime.now() - return self._copy(ObjectValue(newval, ts), ts) + return self._copy(newval) def look_up(self, keys: Dict[InstanceName, ScalarValue]) -> "ArrayEntry": """Return the entry with matching keys. @@ -199,8 +259,10 @@ class InstanceNode: if not isinstance(val, ArrayValue): raise InstanceValueError(self, "entry of non-array") try: - return ArrayEntry(val[:index], val[index+1:], val[index], self, - self.schema_node, val.timestamp) + return ArrayEntry(index, LinkedList.from_list(val[:index]), + LinkedList.from_list(val[index+1:]), + val[index], self, self.schema_node, + val.timestamp) except IndexError: raise NonexistentInstance(self, "entry " + str(index)) from None @@ -215,8 +277,9 @@ class InstanceNode: if not isinstance(val, ArrayValue): raise InstanceValueError(self, "last entry of non-array") try: - return ArrayEntry(val[:-1], [], val[-1], self, - self.schema_node, val.timestamp) + return ArrayEntry( + len(val) - 1, LinkedList.from_list(val[:-1]), + _EmptyList(), val[-1], self,self.schema_node, val.timestamp) except IndexError: raise NonexistentInstance(self, "last of empty") from None @@ -235,8 +298,7 @@ class InstanceNode: raise InstanceValueError(self, "entry of non-array") if index >= len(val): raise NonexistentInstance(self, "entry " + str(index)) from None - ts = datetime.now() - return self._copy(ArrayValue(val[:index] + val[index+1:], ts), ts) + return self._copy(ArrayValue(val[:index] + val[index+1:])) def up(self) -> "InstanceNode": """Return an instance node corresponding to the receiver's parent. @@ -266,7 +328,7 @@ class InstanceNode: Copy of the receiver with the updated value. """ newval = self.schema_node.from_raw(value) if raw else value - return self._copy(newval, datetime.now()) + return self._copy(newval) def goto(self, iroute: "InstanceRoute") -> "InstanceNode": """Move the focus to an instance inside the receiver's value. @@ -436,9 +498,11 @@ class RootNode(InstanceNode): def __init__(self, value: Value, schema_node: "DataNode", timestamp: datetime): - super().__init__(value, None, schema_node, timestamp) - self.name = None # type: None - """The instance name of the root node is always ``None``.""" + self.path = () + self.parinst = None + self.value = value + self.schema_node = schema_node + self.timestamp = timestamp def up(self) -> None: """Override the superclass method. @@ -448,10 +512,9 @@ class RootNode(InstanceNode): """ raise NonexistentInstance(self, "up of top") - def _copy(self, newval: Value = None, - newts: datetime = None) -> InstanceNode: - return RootNode(newval if newval else self.value, self.schema_node, - newts if newts else self._timestamp) + def _copy(self, newval: Value, newts: datetime = None) -> InstanceNode: + return RootNode( + newval, self.schema_node, newts if newts else newval.timestamp) def _ancestors_or_self( self, qname: Union[QualName, bool] = None) -> List["RootNode"]: @@ -469,14 +532,16 @@ class ObjectMember(InstanceNode): def __init__(self, name: InstanceName, siblings: Dict[InstanceName, Value], value: Value, parinst: InstanceNode, schema_node: "DataNode", timestamp: datetime ): - super().__init__(value, parinst, schema_node, timestamp) - self.name = name # type: InstanceName - """The instance name of the receiver.""" + super().__init__(name, value, parinst, schema_node, timestamp) self.siblings = siblings # type: Dict[InstanceName, Value] """Sibling members within the parent object.""" @property - def qual_name(self) -> Optional[QualName]: + def name(self) -> YangIdentifier: + return self.path[-1] + + @property + def qual_name(self) -> QualName: """Return the receiver's qualified name.""" p, s, loc = self.name.partition(":") return (loc, p) if s else (p, self.namespace) @@ -488,7 +553,8 @@ class ObjectMember(InstanceNode): name: Instance name of the sibling member. Raises: - NonexistentSchemaNode: If member `name` is not permitted by the schema. + NonexistentSchemaNode: If member `name` is not permitted by the + schema. NonexistentInstance: If sibling member `name` doesn't exist. """ ssn = self.parinst._member_schema_node(name) @@ -507,15 +573,15 @@ class ObjectMember(InstanceNode): res[self.name] = self.value return res - def _pointer_fragment(self) -> str: - return self.name - - def _copy(self, newval: Value = None, - newts: datetime = None) -> "ObjectMember": - return ObjectMember(self.name, self.siblings, - self.value if newval is None else newval, - self.parinst, self.schema_node, - newts if newts else self._timestamp) + def _copy(self, newval: Value, newts: datetime = None) -> "ObjectMember": + if newts: + ts = newts + elif isinstance(newval, StructuredValue): + ts = newval.timestamp + else: + ts = datetime.now() + return ObjectMember(self.name, self.siblings, newval, self.parinst, + self.schema_node, ts) def _ancestors_or_self( self, qname: Union[QualName, bool] = None) -> List[InstanceNode]: @@ -531,23 +597,23 @@ class ObjectMember(InstanceNode): class ArrayEntry(InstanceNode): """This class represents an array entry.""" - def __init__(self, before: List[Value], after: List[Value], - value: Value, parinst: InstanceNode, - schema_node: "DataNode", timestamp: datetime = None): - super().__init__(value, parinst, schema_node, timestamp) - self.before = before # type: List[Value] + def __init__(self, index: int, before: LinkedList, after: LinkedList, + value: Value, parinst: InstanceNode, + schema_node: "DataNode", timestamp: datetime = None): + super().__init__(index, value, parinst, schema_node, timestamp) + self.before = before # type: LinkedList """Preceding entries of the parent array.""" - self.after = after # type: List[Value] + self.after = after # type: LinkedList """Following entries of the parent array.""" @property def index(self) -> int: - """Return the receiver's index.""" - return len(self.before) + """Index of the receiver in the parent array.""" + return self.path[-1] @property def name(self) -> InstanceName: - """Return the name of the receiver.""" + """Name of the receiver.""" return self.parinst.name @property @@ -567,14 +633,16 @@ class ArrayEntry(InstanceNode): """Return an instance node corresponding to the previous entry. Raises: - NonexistentInstance: If the receiver is the first entry of the parent array. + NonexistentInstance: If the receiver is the first entry of the + parent array. """ try: - newval = self.before[-1] + newval, nbef = self.before.pop() except IndexError: raise NonexistentInstance(self, "previous of first") from None - return ArrayEntry(self.before[:-1], [self.value] + self.after, newval, - self.parinst, self.schema_node, self.timestamp) + return ArrayEntry( + self.index - 1, nbef, self.after.cons(self.value), newval, + self.parinst, self.schema_node, self.timestamp) def next(self) -> "ArrayEntry": """Return an instance node corresponding to the next entry. @@ -583,11 +651,12 @@ class ArrayEntry(InstanceNode): NonexistentInstance: If the receiver is the last entry of the parent array. """ try: - newval = self.after[0] + newval, naft = self.after.pop() except IndexError: raise NonexistentInstance(self, "next of last") from None - return ArrayEntry(self.before + [self.value], self.after[1:], newval, - self.parinst, self.schema_node, self.timestamp) + return ArrayEntry( + self.index + 1, self.before.cons(self.value), naft, newval, + self.parinst, self.schema_node, self.timestamp) def insert_before(self, value: Union[RawValue, Value], raw: bool = False) -> "ArrayEntry": @@ -600,9 +669,9 @@ class ArrayEntry(InstanceNode): Returns: An instance node of the new inserted entry. """ - return ArrayEntry(self.before, [self.value] + self.after, - self._cook_value(value, raw), self.parinst, - self.schema_node, datetime.now()) + return ArrayEntry(self.index, self.before, self.after.cons(self.value), + self._cook_value(value, raw), self.parinst, + self.schema_node, datetime.now()) def insert_after(self, value: Union[RawValue, Value], raw: bool = False) -> "ArrayEntry": @@ -615,9 +684,9 @@ class ArrayEntry(InstanceNode): Returns: An instance node of the newly inserted entry. """ - return ArrayEntry(self.before + [self.value], self.after, - self._cook_value(value, raw), self.parinst, - self.schema_node, datetime.now()) + return ArrayEntry(self.index, self.before.cons(self.value), self.after, + self._cook_value(value, raw), self.parinst, + self.schema_node, datetime.now()) def _cook_value(self, value: Union[RawValue, Value], raw: bool) -> Value: return (super(SequenceNode, self.schema_node).from_raw(value) if raw @@ -625,20 +694,21 @@ class ArrayEntry(InstanceNode): def _zip(self) -> ArrayValue: """Zip the receiver into an array and return it.""" - res = ArrayValue(self.before.copy(), self.timestamp) + res = list(self.before) + res.reverse() res.append(self.value) - res += self.after - return res - - def _pointer_fragment(self) -> int: - return str(len(self.before)) - - def _copy(self, newval: Value = None, - newts: datetime = None) -> "ArrayEntry": - return ArrayEntry(self.before, self.after, - newval if newval else self.value, - self.parinst, self.schema_node, - newts if newts else self._timestamp) + res.extend(list(self.after)) + return ArrayValue(res, self.timestamp) + + def _copy(self, newval: Value, newts: datetime = None) -> "ArrayEntry": + if newts: + ts = newts + elif isinstance(newval, StructuredValue): + ts = newval.timestamp + else: + ts = datetime.now() + return ArrayEntry(self.index, self.before, self.after, newval, + self.parinst, self.schema_node, ts) def _ancestors_or_self( self, qname: Union[QualName, bool] = None) -> List[InstanceNode]: @@ -658,7 +728,7 @@ class ArrayEntry(InstanceNode): return [] res = [] ent = self - for i in range(len(self.before)): + for _ in self.before: ent = ent.previous() res.append(ent) return res @@ -670,7 +740,7 @@ class ArrayEntry(InstanceNode): return [] res = [] ent = self - for i in range(len(self.after)): + for _ in self.after: ent = ent.next() res.append(ent) return res diff --git a/yangson/instvalue.py b/yangson/instvalue.py index 15c034e..3c1b97c 100644 --- a/yangson/instvalue.py +++ b/yangson/instvalue.py @@ -29,12 +29,15 @@ from typing import Dict, List, Union from .typealiases import * # Type aliases -Value = Union[ScalarValue, "ArrayValue", "ObjectValue"] +Value = Union[ScalarValue, "StructuredValue"] """All possible types of cooked values (scalar and structured).""" EntryValue = Union[ScalarValue, "ObjectValue"] """Type of the value a list ot leaf-list entry.""" +InstKey = Union[InstanceName, int] +"""Index of an array entry or name of an object member.""" + class StructuredValue: """Abstract class for array and object values.""" @@ -46,14 +49,14 @@ class StructuredValue: """ self.timestamp = ts if ts else datetime.now() - def __setitem__(self, key, value): - super().__setitem__(key, value) - self.timestamp = datetime.now() - def copy(self) -> "StructuredValue": """Return a shallow copy of the receiver.""" return self.__class__(super().copy(), datetime.now()) + def __setitem__(self, key: InstKey, value: Value) -> None: + super().__setitem__(key, value) + self.timestamp = datetime.now() + def __eq__(self, val: "StructuredValue") -> bool: """Return ``True`` if the receiver equal to `val`. @@ -75,7 +78,7 @@ class ArrayValue(StructuredValue, list): def __hash__(self) -> int: """Return hash value for the receiver.""" - return tuple([ x.__hash__() for x in self]).__hash__() + return tuple([x.__hash__() for x in self]).__hash__() class ObjectValue(StructuredValue, dict): """This class represents cooked object values.""" @@ -88,4 +91,4 @@ class ObjectValue(StructuredValue, dict): def __hash__(self) -> int: """Return hash value for the receiver.""" sks = sorted(self.keys()) - return tuple([ (k, self[k].__hash__()) for k in sks ]).__hash__() + return tuple([(k, self[k].__hash__()) for k in sks]).__hash__() diff --git a/yangson/nodeset.py b/yangson/nodeset.py index 6da3952..9621788 100644 --- a/yangson/nodeset.py +++ b/yangson/nodeset.py @@ -40,8 +40,8 @@ def comparison(meth): class NodeSet(list): def union(self, ns: "NodeSet") -> "NodeSet": - paths = set([n.json_pointer() for n in self]) - return self.__class__(self + [n for n in ns if n.json_pointer() not in paths]) + paths = set([n.path for n in self]) + return self.__class__(self + [n for n in ns if n.path not in paths]) def bind(self, trans: NodeExpr) -> "NodeSet": res = self.__class__([]) diff --git a/yangson/xpathast.py b/yangson/xpathast.py index a0a5260..242e4d8 100644 --- a/yangson/xpathast.py +++ b/yangson/xpathast.py @@ -487,7 +487,7 @@ class FuncName(UnaryExpr): raise XPathTypeError(ns) except IndexError: return "" - if node.name is None: return "" + if node.path is (): return "" if self.local: p, s, loc = node.name.partition(":") return loc if s else p -- GitLab