From b50902b8c0f74e8bb6c4642ba18f6709e2ca1dda Mon Sep 17 00:00:00 2001 From: Leonard Binet Date: Mon, 11 May 2020 18:24:17 +0200 Subject: [PATCH 1/4] node clone method --- lighttree/__init__.py | 3 +-- lighttree/node.py | 12 ++++++++++- lighttree/tree.py | 45 ++++++++++++++++++++------------------- tests/test_interactive.py | 3 +-- tests/test_tree.py | 9 ++++---- tests/testing_utils.py | 11 ++++++++-- 6 files changed, 49 insertions(+), 34 deletions(-) diff --git a/lighttree/__init__.py b/lighttree/__init__.py index 6ad9ab6..9dcbd3a 100644 --- a/lighttree/__init__.py +++ b/lighttree/__init__.py @@ -1,5 +1,4 @@ -from .node import Node -from .tree import Tree +from .tree import Tree, Node from .interactive import TreeBasedObj __all__ = ["Tree", "Node", "TreeBasedObj"] diff --git a/lighttree/node.py b/lighttree/node.py index 74a94e8..d7968df 100644 --- a/lighttree/node.py +++ b/lighttree/node.py @@ -1,6 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import unicode_literals +import copy from future.utils import python_2_unicode_compatible, string_types import uuid @@ -27,13 +29,16 @@ def __init__(self, identifier=None, auto_uuid=False, _children=None): # a recursive import error if _children is not None and not isinstance(_children, (list, tuple)): raise ValueError("Invalid children declaration.") - self._children = _children + self._children = _children or [] def line_repr(self, depth, **kwargs): """Control how node is displayed in tree representation. """ return self.identifier + def clone(self, deep=False): + return copy.deepcopy(self) if deep else copy.copy(self) + def serialize(self, *args, **kwargs): return {"identifier": self.identifier} @@ -47,6 +52,11 @@ def deserialize(cls, d, *args, **kwargs): def _deserialize(cls, d, *args, **kwargs): return cls(d.get("identifier")) + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.identifier == other.identifier and self._children == other._children + def __str__(self): return "%s, id=%s" % (self.__class__.__name__, self.identifier) diff --git a/lighttree/tree.py b/lighttree/tree.py index 80a90c2..3dfc2a7 100644 --- a/lighttree/tree.py +++ b/lighttree/tree.py @@ -5,10 +5,9 @@ from future.utils import python_2_unicode_compatible, iteritems from collections import defaultdict -from copy import deepcopy from operator import attrgetter -from .node import Node +from lighttree.node import Node from .utils import STYLES from .exceptions import MultipleRootError, NotFoundNodeError, DuplicatedNodeError @@ -114,11 +113,8 @@ def clone(self, with_tree=True, deep=False, new_root=None): for nid in self.expand_tree(nid=new_root): node = self.get(nid) - if deep: - node = deepcopy(node) pid = None if nid == self.root or nid == new_root else self.parent(nid) - # with_children only makes sense when using "node hierarchy" syntax - new_tree.insert_node(node, parent_id=pid, with_children=False) + new_tree.insert_node(node, parent_id=pid, deep=deep) return new_tree def parent(self, nid, id_only=True): @@ -210,29 +206,34 @@ def insert( '"item" parameter must either be a Node, or a Tree, got <%s>.' % type(item) ) - def insert_node( - self, node, parent_id=None, child_id=None, deep=False, with_children=True - ): + def insert_node(self, node, parent_id=None, child_id=None, deep=False): + """Make a copy of inserted node, and insert it. + + Note: when using "Node hierarchy" syntax, _children attribute of copied node are reset so that insertion occurs + once only. + """ self._validate_node_insertion(node) - node = deepcopy(node) if deep else node + node = node.clone(deep=deep) if parent_id is not None and child_id is not None: raise ValueError('Can declare at most "parent_id" or "child_id"') if child_id is not None: self._insert_node_above(node, child_id=child_id) return self - self._insert_node_below(node, parent_id=parent_id, with_children=with_children) + self._insert_node_below(node, parent_id=parent_id) return self - def _insert_node_below(self, node, parent_id, with_children=True): + def _insert_node_below(self, node, parent_id): # insertion at root if parent_id is None: if not self.is_empty(): raise MultipleRootError("A tree takes one root merely.") self.root = node.identifier self._nodes_map[node.identifier] = node - if with_children and hasattr(node, "_children"): - for child in node._children or []: - self.insert(child, parent_id=node.identifier) + for child in node._children: + self.insert(child, parent_id=node.identifier) + # reset _children attribute so that children nodes cannot be inserted multiple times + # rely on the fact that inserted nodes are copies, as handled in insert_node method + node._children = [] return self._ensure_present(parent_id) @@ -240,9 +241,11 @@ def _insert_node_below(self, node, parent_id, with_children=True): self._nodes_map[node_id] = node self._nodes_parent[node_id] = parent_id self._nodes_children[parent_id].add(node_id) - if with_children and hasattr(node, "_children"): - for child in node._children or []: - self.insert(child, parent_id=node.identifier) + for child in node._children or []: + self.insert(child, parent_id=node.identifier) + # reset _children attribute so that children nodes cannot be inserted multiple times + # rely on the fact that inserted nodes are copies, as handled in insert_node method + node._children = [] def _insert_node_above(self, node, child_id): self._ensure_present(child_id) @@ -280,10 +283,8 @@ def _insert_tree_below(self, new_tree, parent_id, deep): for new_nid in new_tree.expand_tree(): node = new_tree.get(new_nid) pid = parent_id if new_nid == new_tree.root else new_tree.parent(new_nid) - # with_children only makes sense when using "node hierarchy" syntax - self.insert_node( - deepcopy(node) if deep else node, parent_id=pid, with_children=False - ) + # node copy is handled in insert_node method + self.insert_node(node, parent_id=pid, deep=deep) return self def _insert_tree_above(self, new_tree, child_id, child_id_below, deep): diff --git a/tests/test_interactive.py b/tests/test_interactive.py index a863037..311e074 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -188,8 +188,7 @@ class InteractiveTree(TreeBasedObj): └── a2 """ self.assertTrue(hasattr(a, "a1")) - # check that initial tree, and child tree reference the same nodes - self.assertIs(a._tree.get("a1"), obj._tree.get("a1")) + self.assertEqual(a._tree.get("a1"), obj._tree.get("a1")) # test representations self.assertEqual( diff --git a/tests/test_tree.py b/tests/test_tree.py index a22d0fb..1cc80b7 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -70,7 +70,7 @@ def test_insert_node_below(self): node_a = Node("a") t.insert_node(node_a, parent_id="root_id") self.assertSetEqual(set(t._nodes_map.keys()), {"root_id", "a"}) - self.assertIs(t._nodes_map["a"], node_a) + self.assertEqual(t._nodes_map["a"], node_a) self.assertEqual(t._nodes_parent["root_id"], None) self.assertEqual(t._nodes_parent["a"], "root_id") self.assertSetEqual(t._nodes_children["a"], set()) @@ -245,7 +245,7 @@ def test_clone_with_tree(self): self.assertIs(t.mutable_object, t_shallow_clone.mutable_object) # nodes are shallow copies for nid, node in iteritems(t._nodes_map): - self.assertIs(t_shallow_clone._nodes_map[nid], node) + self.assertEqual(t_shallow_clone._nodes_map[nid], node) tree_sanity_check(t) tree_sanity_check(t_shallow_clone) @@ -637,7 +637,7 @@ def test_insert_tree_below(self): ) self.assertTrue(all(nid in t for nid in ("c", "c1", "c2", "c12"))) # by default pasted new tree is a shallow copy - self.assertIs(t.get("c"), t_to_paste.get("c")) + self.assertEqual(t.get("c"), t_to_paste.get("c")) # cannot repaste tree, because then there would be node duplicates with self.assertRaises(DuplicatedNodeError): @@ -779,8 +779,7 @@ def test_merge(self): # new tree root is not conserved self.assertTrue("c" not in t) self.assertTrue(all(nid in t for nid in ("c1", "c2", "c12"))) - # by default merged new tree is a shallow copy - self.assertIs(t.get("c1"), t_to_merge.get("c1")) + self.assertEqual(t.get("c1"), t_to_merge.get("c1")) # cannot remerge tree, because then there would be node duplicates with self.assertRaises(DuplicatedNodeError): diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 49fd422..254ea2c 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -98,9 +98,16 @@ def get_sample_custom_tree(): class CustomNode(Node): - def __init__(self, identifier, key): + def __init__(self, identifier, key, _children=None): self.key = key - super(CustomNode, self).__init__(identifier=identifier) + super(CustomNode, self).__init__(identifier=identifier, _children=_children) + + def clone(self, deep=False): + return self.__class__( + identifier=self.identifier, + _children=[c.clone(deep=deep) for c in self._children], + key=self.key, + ) def serialize(self, *args, **kwargs): with_key = kwargs.pop("with_key", None) From 44bc51a97fbe9339a2402e3f3bd4f939e1cd41c5 Mon Sep 17 00:00:00 2001 From: Leonard Binet Date: Mon, 11 May 2020 22:04:28 +0200 Subject: [PATCH 2/4] v0.0.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ef66c1..184202d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup -__version__ = "0.0.3" +__version__ = "0.0.4" setup( From a2b6d6b99bc169fa701616316d5c7c0d9f8a3f42 Mon Sep 17 00:00:00 2001 From: Leonard Binet Date: Mon, 25 May 2020 09:56:33 +0200 Subject: [PATCH 3/4] remove node hierarchy syntax --- lighttree/node.py | 10 ++------ lighttree/tree.py | 12 +--------- tests/test_tree.py | 53 ------------------------------------------ tests/testing_utils.py | 10 +++----- 4 files changed, 6 insertions(+), 79 deletions(-) diff --git a/lighttree/node.py b/lighttree/node.py index d7968df..e36f856 100644 --- a/lighttree/node.py +++ b/lighttree/node.py @@ -10,7 +10,7 @@ @python_2_unicode_compatible class Node(object): - def __init__(self, identifier=None, auto_uuid=False, _children=None): + def __init__(self, identifier=None, auto_uuid=False): """ :param identifier: node identifier, must be unique per tree """ @@ -24,12 +24,6 @@ def __init__(self, identifier=None, auto_uuid=False, _children=None): raise ValueError("Required identifier") identifier = uuid.uuid4() self.identifier = identifier - # children type is not checked here, it is at insertion in tree - # only allowed types should be Node, or Tree, but cannot ensure whether it's a Tree since it would cause - # a recursive import error - if _children is not None and not isinstance(_children, (list, tuple)): - raise ValueError("Invalid children declaration.") - self._children = _children or [] def line_repr(self, depth, **kwargs): """Control how node is displayed in tree representation. @@ -55,7 +49,7 @@ def _deserialize(cls, d, *args, **kwargs): def __eq__(self, other): if not isinstance(other, self.__class__): return False - return self.identifier == other.identifier and self._children == other._children + return self.identifier == other.identifier def __str__(self): return "%s, id=%s" % (self.__class__.__name__, self.identifier) diff --git a/lighttree/tree.py b/lighttree/tree.py index 3dfc2a7..f4c9379 100644 --- a/lighttree/tree.py +++ b/lighttree/tree.py @@ -71,7 +71,7 @@ def _validate_node_insertion(self, node): ) def _validate_tree_insertion(self, tree): - if not isinstance(tree, self.__class__): + if not isinstance(tree, Tree): raise ValueError( "Tree must be instance of <%s>, got <%s>" % (self.__class__.__name__, type(tree)) @@ -229,11 +229,6 @@ def _insert_node_below(self, node, parent_id): raise MultipleRootError("A tree takes one root merely.") self.root = node.identifier self._nodes_map[node.identifier] = node - for child in node._children: - self.insert(child, parent_id=node.identifier) - # reset _children attribute so that children nodes cannot be inserted multiple times - # rely on the fact that inserted nodes are copies, as handled in insert_node method - node._children = [] return self._ensure_present(parent_id) @@ -241,11 +236,6 @@ def _insert_node_below(self, node, parent_id): self._nodes_map[node_id] = node self._nodes_parent[node_id] = parent_id self._nodes_children[parent_id].add(node_id) - for child in node._children or []: - self.insert(child, parent_id=node.identifier) - # reset _children attribute so that children nodes cannot be inserted multiple times - # rely on the fact that inserted nodes are copies, as handled in insert_node method - node._children = [] def _insert_node_above(self, node, child_id): self._ensure_present(child_id) diff --git a/tests/test_tree.py b/tests/test_tree.py index 1cc80b7..ff2e1aa 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -887,58 +887,5 @@ def test_drop_subtree(self): """a1 ├── a11 └── a12 -""", - ) - - def test_node_hierarchy_deserialization(self): - node_hierarchy = Node( - identifier="root", - _children=[ - Node( - identifier="a", - _children=[Node(identifier="a1"), Node(identifier="a2")], - ), - Node(identifier="b", _children=[Node(identifier="b1")]), - ], - ) - t = Tree() - t.insert(node_hierarchy) - self.assertEqual( - t.show(), - """root -├── a -│ ├── a1 -│ └── a2 -└── b - └── b1 -""", - ) - - def test_node_hierarchy_with_tree_deserialization(self): - node_hierarchy = Node( - identifier="root", - _children=[ - Node( - identifier="a", - _children=[Node(identifier="a1"), Node(identifier="a2")], - ), - Node(identifier="b", _children=[Node(identifier="b1")]), - get_sample_tree_2(), - ], - ) - t = Tree() - t.insert(node_hierarchy) - self.assertEqual( - t.show(), - """root -├── a -│ ├── a1 -│ └── a2 -├── b -│ └── b1 -└── c - ├── c1 - │ └── c12 - └── c2 """, ) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 254ea2c..00f204d 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -98,16 +98,12 @@ def get_sample_custom_tree(): class CustomNode(Node): - def __init__(self, identifier, key, _children=None): + def __init__(self, identifier, key): self.key = key - super(CustomNode, self).__init__(identifier=identifier, _children=_children) + super(CustomNode, self).__init__(identifier=identifier) def clone(self, deep=False): - return self.__class__( - identifier=self.identifier, - _children=[c.clone(deep=deep) for c in self._children], - key=self.key, - ) + return self.__class__(identifier=self.identifier, key=self.key,) def serialize(self, *args, **kwargs): with_key = kwargs.pop("with_key", None) From 1f114624187aeb08c5a93d1ea826146c1e3965f9 Mon Sep 17 00:00:00 2001 From: Leonard Binet Date: Mon, 25 May 2020 09:56:46 +0200 Subject: [PATCH 4/4] v0.0.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 184202d..1b9d641 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup -__version__ = "0.0.4" +__version__ = "0.0.5" setup(