From bf37e2dab8c15533883e1e51635e8b28cc78d11b Mon Sep 17 00:00:00 2001 From: Licini Date: Tue, 18 Jul 2023 18:01:27 +0200 Subject: [PATCH 01/14] Tree classes --- src/compas/datastructures/tree/__init__.py | 0 src/compas/datastructures/tree/tree.py | 76 ++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/compas/datastructures/tree/__init__.py create mode 100644 src/compas/datastructures/tree/tree.py diff --git a/src/compas/datastructures/tree/__init__.py b/src/compas/datastructures/tree/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py new file mode 100644 index 00000000000..c8cc4c74898 --- /dev/null +++ b/src/compas/datastructures/tree/tree.py @@ -0,0 +1,76 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.data import Data +from compas.datastructures import Graph + + +class TreeNode(Data): + + JSONSCHEMA = {} + + def __init__(self, tree): + super(TreeNode, self).__init__() + self._tree = tree + self._attributes = {} + self._parent = None + + @property + def tree(self): + return self._tree + + @property + def attributes(self): + return self._attributes + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, node): + assert node is None or isinstance(node, TreeNode) + self._parent = node + + @property + def children(self): + pass + + @property + def acestors(self): + pass + + @property + def descendants(self): + pass + + def add_child(self, node): + node._parent = self + + + +class Tree(Graph): + + JSONSCHEMA = {} + + def __init__(self): + super(Tree, self).__init__() + self._attributes = {} + assert self.is_valid + + @property + def is_valid(self): + pass + + @property + def attributes(self): + return self._attributes + + @property + def root(self): + pass + + @property + def leaves(self): + pass \ No newline at end of file From 916f8088f11451dd294a4472fc62e9a5e3713ac2 Mon Sep 17 00:00:00 2001 From: Licini Date: Thu, 20 Jul 2023 15:26:37 +0200 Subject: [PATCH 02/14] more apis --- src/compas/datastructures/tree/tree.py | 155 +++++++++++++++++++------ 1 file changed, 122 insertions(+), 33 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index c8cc4c74898..f4549a626f8 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -1,76 +1,165 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import division +from __future__ import annotations from compas.data import Data -from compas.datastructures import Graph +from compas.datastructures import Datastructure class TreeNode(Data): - JSONSCHEMA = {} - def __init__(self, tree): - super(TreeNode, self).__init__() - self._tree = tree - self._attributes = {} + def __init__(self, name: str = None, attributes: dict = None): + super(TreeNode, self).__init__(name=name) + self.attributes = attributes or {} self._parent = None + self._children = set() + + def __repr__(self) -> str: + return "TreeNode({})".format(self.name) + + @property + def data(self): + return { + "name": self.name, + "attributes": self.attributes, + "parent": str(self.parent.guid) if self.parent else None, + "children": [str(child.guid) for child in self.children], + } @property - def tree(self): - return self._tree + def is_root(self): + return self._parent is None @property - def attributes(self): - return self._attributes + def is_leaf(self): + return not self._children + + @property + def is_branch(self): + return not self.is_root and not self.is_leaf @property def parent(self): return self._parent - - @parent.setter - def parent(self, node): - assert node is None or isinstance(node, TreeNode) - self._parent = node @property def children(self): - pass + return list(self._children) + + def add(self, node: TreeNode): + assert isinstance(node, TreeNode), "The node is not a TreeNode object." + self._children.add(node) + node._parent = self + + @property + def remove(self, node: TreeNode): + self._children.remove(node) + node._parent = None @property def acestors(self): - pass + this = self + while this: + yield this + this = this.parent @property def descendants(self): - pass - - def add_child(self, node): - node._parent = self + for child in self.children: + yield child + for descendant in child.descendants: + yield descendant + def traverse(self): + yield self + for descendant in self.descendants: + yield descendant -class Tree(Graph): - +class Tree(Datastructure): JSONSCHEMA = {} def __init__(self): super(Tree, self).__init__() - self._attributes = {} - assert self.is_valid - - @property - def is_valid(self): - pass + self.attributes = {} + self._root = None @property - def attributes(self): - return self._attributes + def data(self): + return { + "root": str(self.root.guid) if self.root else None, + "nodes": [node.data for node in self.nodes], + } @property def root(self): - pass + return self._root + + def add_root(self, node: TreeNode): + assert isinstance(node, TreeNode), "The node is not a TreeNode object." + if not node.is_root: + raise ValueError("The node is already part of another tree.") + self._root = node + + def add(self, node: TreeNode, parent: TreeNode): + assert isinstance(node, TreeNode), "The node is not a TreeNode object." + if self.root is None: + raise ValueError("The tree has no root node, use add_root() first.") + else: + parent.add(node) + + @property + def nodes(self): + return list(self.root.traverse()) + + def remove(self, node: TreeNode): + if node.is_root: + self._root = None + else: + node.parent.remove(node) @property def leaves(self): - pass \ No newline at end of file + for node in self.nodes: + if node.is_leaf: + yield node + + def summary(self): + nodes = len(self.nodes) + branches = sum(1 for node in self.nodes if node.is_branch) + leaves = sum(1 for node in self.nodes if node.is_leaf) + print("Tree with {} nodes, {} branches, and {} leaves".format(nodes, branches, leaves)) + + def print(self): + def _print(node, depth=0): + print(" " * depth + str(node)) + for child in node.children: + _print(child, depth + 1) + + _print(self.root) + + +if __name__ == "__main__": + from compas.data import json_dumps + + R = TreeNode("R") + B = TreeNode("B") + L = TreeNode("L") + L2 = TreeNode("L2") + + T = Tree() + T.add_root(R) + + # T.add(B, R) + # T.add(L, B) + # T.add(L2, B) + + R.add(B) + B.add(L) + B.add(L2) + + # T.print() + + print(json_dumps(T, pretty=True)) From a16d86fb568847050763adcdad9e790a2c84347e Mon Sep 17 00:00:00 2001 From: Licini Date: Fri, 21 Jul 2023 16:46:35 +0200 Subject: [PATCH 03/14] Docs --- CHANGELOG.md | 1 + src/compas/datastructures/__init__.py | 5 + src/compas/datastructures/tree/tree.py | 198 ++++++++++++++++++------- 3 files changed, 152 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 643bc6867b0..abe25970f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.trimesh_descent_numpy`. * Added `compas.geometry.trimesh_gradient_numpy`. * Added a deprecation warning when using `Artist` for `Plotter`. +* Added `compas.datastructures.Tree` and `compas.datastructures.TreeNode` classes. ### Changed diff --git a/src/compas/datastructures/__init__.py b/src/compas/datastructures/__init__.py index 32a084d67b2..3fa51ddfb7c 100644 --- a/src/compas/datastructures/__init__.py +++ b/src/compas/datastructures/__init__.py @@ -157,6 +157,8 @@ from .assembly.assembly import Assembly from .assembly.part import Feature, GeometricFeature, ParametricFeature, Part +from .tree.tree import Tree, TreeNode + BaseNetwork = Network BaseMesh = Mesh BaseVolMesh = VolMesh @@ -274,6 +276,9 @@ "Feature", "GeometricFeature", "ParametricFeature", + # Trees + "Tree", + "TreeNode", ] if not compas.IPY: diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index f4549a626f8..150da33b667 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -3,30 +3,53 @@ from __future__ import division from __future__ import annotations -from compas.data import Data from compas.datastructures import Datastructure -class TreeNode(Data): - JSONSCHEMA = {} +class TreeNode(object): + """A node of a tree data structure. + + Parameters + ---------- + name: str, optional + The name of the tree ndoe. + attributes: dict[str, Any], optional + User-defined attributes of the datastructure. + + Attributes + ---------- + name: str + The name of the datastructure. + attributes : dict[str, Any] + User-defined attributes of the datastructure. + parent: :class:`~compas.datastructures.TreeNode` + The parent node of the tree node. + children: set[:class:`~compas.datastructures.TreeNode`] + The children of the tree node. + tree: :class:`~compas.datastructures.Tree` + The tree the node belongs to. + is_root: bool + True if the node is the root node of the tree. + is_leaf: bool + True if the node is a leaf node of the tree. + is_branch: bool + True if the node is a branch node of the tree. + acestors: generator[:class:`~compas.datastructures.TreeNode`] + A generator of the acestors of the tree node. + descendants: generator[:class:`~compas.datastructures.TreeNode`] + A generator of the descendants of the tree node. + + """ def __init__(self, name: str = None, attributes: dict = None): - super(TreeNode, self).__init__(name=name) + self.name = name self.attributes = attributes or {} self._parent = None self._children = set() + self._tree = None def __repr__(self) -> str: - return "TreeNode({})".format(self.name) - - @property - def data(self): - return { - "name": self.name, - "attributes": self.attributes, - "parent": str(self.parent.guid) if self.parent else None, - "children": [str(child.guid) for child in self.children], - } + return "".format(self.name) @property def is_root(self): @@ -46,17 +69,29 @@ def parent(self): @property def children(self): - return list(self._children) + return self._children + + @property + def tree(self): + return self._tree def add(self, node: TreeNode): - assert isinstance(node, TreeNode), "The node is not a TreeNode object." + """Add a child node to this node.""" + if not isinstance(node, TreeNode): + raise TypeError("The node is not a TreeNode object.") self._children.add(node) node._parent = self + node._tree = self.tree + if self.tree: + self.tree.nodes.add(node) - @property def remove(self, node: TreeNode): + """Remove a child node from this node.""" self._children.remove(node) node._parent = None + node._tree = None + if self.tree: + self.tree.nodes.remove(node) @property def acestors(self): @@ -73,38 +108,112 @@ def descendants(self): yield descendant def traverse(self): + """Traverse the tree from this node.""" yield self for descendant in self.descendants: yield descendant class Tree(Datastructure): + """A tree data structure. + + Parameters + ---------- + name: str, optional + The name of the datastructure. + attributes: dict[str, Any], optional + User-defined attributes of the datastructure. + + Attributes + ---------- + name: str + The name of the datastructure. + attributes : dict[str, Any] + User-defined attributes of the datastructure. + root: :class:`~compas.datastructures.TreeNode` + The root node of the tree. + nodes: set[:class:`~compas.datastructures.TreeNode`] + The nodes of the tree. + leaves: generator[:class:`~compas.datastructures.TreeNode`] + A generator of the leaves of the tree. + + Examples + -------- + >>> from compas.datastructures import Tree, TreeNode + >>> tree = Tree() + >>> root = TreeNode('root') + >>> branch = TreeNode('branch') + >>> leaf1 = TreeNode('leaf1') + >>> leaf2 = TreeNode('leaf2') + >>> tree.add_root(root) + >>> root.add(branch) + >>> branch.add(leaf1) + >>> branch.add(leaf2) + >>> print(tree) + + >>> tree.print() + + + + + + """ + JSONSCHEMA = {} - def __init__(self): + def __init__(self, name: str = None, attributes: dict = None): super(Tree, self).__init__() - self.attributes = {} + self.name = name + self.attributes = attributes or {} self._root = None + self._nodes = set() @property def data(self): + def get_node_data(node): + return { + "name": node.name, + "attributes": node.attributes, + "children": [get_node_data(child) for child in node.children], + } + return { - "root": str(self.root.guid) if self.root else None, - "nodes": [node.data for node in self.nodes], + "name": self.name, + "root": get_node_data(self.root), + "attributes": self.attributes, } + @data.setter + def data(self, data): + self.name = data["name"] + self.attributes = data["attributes"] + + def node_from_data(data): + node = TreeNode(data["name"], data["attributes"]) + for child in data["children"]: + node.add(node_from_data(child)) + return node + + self.add_root(node_from_data(data["root"])) + @property def root(self): return self._root def add_root(self, node: TreeNode): - assert isinstance(node, TreeNode), "The node is not a TreeNode object." + """Add a root node to the tree.""" + if not isinstance(node, TreeNode): + raise TypeError("The node is not a TreeNode object.") if not node.is_root: raise ValueError("The node is already part of another tree.") self._root = node + node._tree = self + self._nodes.add(node) def add(self, node: TreeNode, parent: TreeNode): - assert isinstance(node, TreeNode), "The node is not a TreeNode object." + """Add a node to the tree.""" + if not isinstance(node, TreeNode): + raise TypeError("The node is not a TreeNode object.") if self.root is None: raise ValueError("The tree has no root node, use add_root() first.") else: @@ -112,11 +221,18 @@ def add(self, node: TreeNode, parent: TreeNode): @property def nodes(self): - return list(self.root.traverse()) + return self._nodes + + def remove_root(self): + """Remove the root node from the tree.""" + self._root._tree = None + self.nodes.remove(self._root) + self._root = None def remove(self, node: TreeNode): - if node.is_root: - self._root = None + """Remove a node from the tree.""" + if node == self.root: + self.remove_root() else: node.parent.remove(node) @@ -126,40 +242,18 @@ def leaves(self): if node.is_leaf: yield node - def summary(self): + def __repr__(self): nodes = len(self.nodes) branches = sum(1 for node in self.nodes if node.is_branch) leaves = sum(1 for node in self.nodes if node.is_leaf) - print("Tree with {} nodes, {} branches, and {} leaves".format(nodes, branches, leaves)) + return "".format(nodes, branches, leaves) def print(self): + """Print the spatial hierarchy of the tree.""" + def _print(node, depth=0): print(" " * depth + str(node)) for child in node.children: _print(child, depth + 1) _print(self.root) - - -if __name__ == "__main__": - from compas.data import json_dumps - - R = TreeNode("R") - B = TreeNode("B") - L = TreeNode("L") - L2 = TreeNode("L2") - - T = Tree() - T.add_root(R) - - # T.add(B, R) - # T.add(L, B) - # T.add(L2, B) - - R.add(B) - B.add(L) - B.add(L2) - - # T.print() - - print(json_dumps(T, pretty=True)) From 0b772d2bddb9cd2b5fbee959eb87b506e87bd8f9 Mon Sep 17 00:00:00 2001 From: Licini Date: Wed, 26 Jul 2023 15:05:03 +0200 Subject: [PATCH 04/14] adjustments based on feedback --- src/compas/datastructures/tree/tree.py | 37 +++++++++++++++----------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 150da33b667..3ca2324628d 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -1,7 +1,6 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import division -from __future__ import annotations from compas.datastructures import Datastructure @@ -41,14 +40,14 @@ class TreeNode(object): """ - def __init__(self, name: str = None, attributes: dict = None): + def __init__(self, name=None, attributes=None): self.name = name self.attributes = attributes or {} self._parent = None self._children = set() self._tree = None - def __repr__(self) -> str: + def __repr__(self): return "".format(self.name) @property @@ -75,7 +74,7 @@ def children(self): def tree(self): return self._tree - def add(self, node: TreeNode): + def add(self, node): """Add a child node to this node.""" if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") @@ -85,7 +84,7 @@ def add(self, node: TreeNode): if self.tree: self.tree.nodes.add(node) - def remove(self, node: TreeNode): + def remove(self, node): """Remove a child node from this node.""" self._children.remove(node) node._parent = None @@ -94,7 +93,7 @@ def remove(self, node: TreeNode): self.tree.nodes.remove(node) @property - def acestors(self): + def ancestors(self): this = self while this: yield this @@ -107,11 +106,20 @@ def descendants(self): for descendant in child.descendants: yield descendant - def traverse(self): + def traverse(self, strategy="preorder"): """Traverse the tree from this node.""" - yield self - for descendant in self.descendants: - yield descendant + if strategy == "preorder": + yield self + for child in self.children: + for node in child.traverse(strategy): + yield node + elif strategy == "postorder": + for child in self.children: + for node in child.traverse(strategy): + yield node + yield self + else: + raise ValueError("Unknown traversal strategy: {}".format(strategy)) class Tree(Datastructure): @@ -223,7 +231,7 @@ def add(self, node: TreeNode, parent: TreeNode): def nodes(self): return self._nodes - def remove_root(self): + def _remove_root(self): """Remove the root node from the tree.""" self._root._tree = None self.nodes.remove(self._root) @@ -232,7 +240,7 @@ def remove_root(self): def remove(self, node: TreeNode): """Remove a node from the tree.""" if node == self.root: - self.remove_root() + self._remove_root() else: node.parent.remove(node) @@ -243,10 +251,7 @@ def leaves(self): yield node def __repr__(self): - nodes = len(self.nodes) - branches = sum(1 for node in self.nodes if node.is_branch) - leaves = sum(1 for node in self.nodes if node.is_leaf) - return "".format(nodes, branches, leaves) + return "".format(len(self.nodes)) def print(self): """Print the spatial hierarchy of the tree.""" From 90594eb6f52d1ca46cf5bc45c86b123ba33f96e9 Mon Sep 17 00:00:00 2001 From: Licini Date: Wed, 26 Jul 2023 15:13:44 +0200 Subject: [PATCH 05/14] remove type hints --- src/compas/datastructures/tree/tree.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 3ca2324628d..d69e216c84f 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -169,7 +169,7 @@ class Tree(Datastructure): JSONSCHEMA = {} - def __init__(self, name: str = None, attributes: dict = None): + def __init__(self, name=None, attributes=None): super(Tree, self).__init__() self.name = name self.attributes = attributes or {} @@ -208,7 +208,7 @@ def node_from_data(data): def root(self): return self._root - def add_root(self, node: TreeNode): + def add_root(self, node): """Add a root node to the tree.""" if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") @@ -218,7 +218,7 @@ def add_root(self, node: TreeNode): node._tree = self self._nodes.add(node) - def add(self, node: TreeNode, parent: TreeNode): + def add(self, node, parent): """Add a node to the tree.""" if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") @@ -237,7 +237,7 @@ def _remove_root(self): self.nodes.remove(self._root) self._root = None - def remove(self, node: TreeNode): + def remove(self, node): """Remove a node from the tree.""" if node == self.root: self._remove_root() From 42ca1eacba7d1d292831940e21dd33243dad0f5d Mon Sep 17 00:00:00 2001 From: Li Chen Date: Thu, 5 Oct 2023 14:35:58 +0200 Subject: [PATCH 06/14] docstring fixes --- src/compas/datastructures/tree/tree.py | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index d69e216c84f..fac4dd02b9b 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -10,32 +10,32 @@ class TreeNode(object): Parameters ---------- - name: str, optional + name : str, optional The name of the tree ndoe. - attributes: dict[str, Any], optional + attributes : dict[str, Any], optional User-defined attributes of the datastructure. Attributes ---------- - name: str + name : str The name of the datastructure. attributes : dict[str, Any] User-defined attributes of the datastructure. - parent: :class:`~compas.datastructures.TreeNode` + parent : :class:`~compas.datastructures.TreeNode` The parent node of the tree node. - children: set[:class:`~compas.datastructures.TreeNode`] + children : set[:class:`~compas.datastructures.TreeNode`] The children of the tree node. - tree: :class:`~compas.datastructures.Tree` + tree : :class:`~compas.datastructures.Tree` The tree the node belongs to. - is_root: bool + is_root : bool True if the node is the root node of the tree. - is_leaf: bool + is_leaf : bool True if the node is a leaf node of the tree. - is_branch: bool + is_branch : bool True if the node is a branch node of the tree. - acestors: generator[:class:`~compas.datastructures.TreeNode`] + acestors : generator[:class:`~compas.datastructures.TreeNode`] A generator of the acestors of the tree node. - descendants: generator[:class:`~compas.datastructures.TreeNode`] + descendants : generator[:class:`~compas.datastructures.TreeNode`] A generator of the descendants of the tree node. """ @@ -127,22 +127,22 @@ class Tree(Datastructure): Parameters ---------- - name: str, optional + name : str, optional The name of the datastructure. - attributes: dict[str, Any], optional + attributes : dict[str, Any], optional User-defined attributes of the datastructure. Attributes ---------- - name: str + name : str The name of the datastructure. attributes : dict[str, Any] User-defined attributes of the datastructure. - root: :class:`~compas.datastructures.TreeNode` + root : :class:`~compas.datastructures.TreeNode` The root node of the tree. - nodes: set[:class:`~compas.datastructures.TreeNode`] + nodes : set[:class:`~compas.datastructures.TreeNode`] The nodes of the tree. - leaves: generator[:class:`~compas.datastructures.TreeNode`] + leaves : generator[:class:`~compas.datastructures.TreeNode`] A generator of the leaves of the tree. Examples From 3441105f1c15cd50fa46a36a7d3945a3816ec77a Mon Sep 17 00:00:00 2001 From: Li Chen Date: Thu, 5 Oct 2023 14:47:53 +0200 Subject: [PATCH 07/14] align with new data mechanism --- src/compas/datastructures/tree/tree.py | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index fac4dd02b9b..18c8a7fb6f1 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -74,6 +74,21 @@ def children(self): def tree(self): return self._tree + @property + def data(self): + return { + "name": self.name, + "attributes": self.attributes, + "children": [child.data for child in self.children], + } + + @classmethod + def from_data(cls, data): + node = cls(data["name"], data["attributes"]) + for child in data["children"]: + node.add(cls.from_data(child)) + return node + def add(self, node): """Add a child node to this node.""" if not isinstance(node, TreeNode): @@ -178,31 +193,18 @@ def __init__(self, name=None, attributes=None): @property def data(self): - def get_node_data(node): - return { - "name": node.name, - "attributes": node.attributes, - "children": [get_node_data(child) for child in node.children], - } - return { "name": self.name, - "root": get_node_data(self.root), + "root": self.root.data, "attributes": self.attributes, } - @data.setter - def data(self, data): - self.name = data["name"] - self.attributes = data["attributes"] - - def node_from_data(data): - node = TreeNode(data["name"], data["attributes"]) - for child in data["children"]: - node.add(node_from_data(child)) - return node - - self.add_root(node_from_data(data["root"])) + @classmethod + def from_data(cls, data): + tree = cls(data["name"], data["attributes"]) + root = TreeNode.from_data(data["root"]) + tree.add_root(root) + return tree @property def root(self): From 98dd468d6d2f51426c26be31fde7eae35e9c39c4 Mon Sep 17 00:00:00 2001 From: Li Chen Date: Fri, 6 Oct 2023 11:03:15 +0200 Subject: [PATCH 08/14] adjust tree functions --- CHANGELOG.md | 2 + src/compas/datastructures/tree/tree.py | 144 ++++++++++++++++++++----- 2 files changed, 119 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e94db1f1c0..172cbed3c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added `compas.datastructures.TreeNode` and `compas.datastructures.Tree` classes. + ### Changed * Changed `Network.is_planar` to rely on `NetworkX` instead `planarity` for planarity checking. diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 18c8a7fb6f1..5563a17bfb7 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -3,9 +3,10 @@ from __future__ import division from compas.datastructures import Datastructure +from compas.data import Data -class TreeNode(object): +class TreeNode(Data): """A node of a tree data structure. Parameters @@ -90,7 +91,24 @@ def from_data(cls, data): return node def add(self, node): - """Add a child node to this node.""" + """ + Add a child node to this node. + + Parameters + ---------- + node : :class:`~compas.datastructures.TreeNode` + The node to add. + + Returns + ------- + None + + Raises + ------ + TypeError + If the node is not a :class:`~compas.datastructures.TreeNode` object. + + """ if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") self._children.add(node) @@ -100,7 +118,19 @@ def add(self, node): self.tree.nodes.add(node) def remove(self, node): - """Remove a child node from this node.""" + """ + Remove a child node from this node. + + Parameters + ---------- + node : :class:`~compas.datastructures.TreeNode` + The node to remove. + + Returns + ------- + None + + """ self._children.remove(node) node._parent = None node._tree = None @@ -122,7 +152,26 @@ def descendants(self): yield descendant def traverse(self, strategy="preorder"): - """Traverse the tree from this node.""" + """ + Traverse the tree from this node. + + Parameters + ---------- + strategy : str, optional + The traversal strategy. Options are ``"preorder"`` and ``"postorder"``. + Default is ``"preorder"``. + + Yields + ------ + :class:`~compas.datastructures.TreeNode` + The next node in the traversal. + + Raises + ------ + ValueError + If the strategy is not ``"preorder"`` or ``"postorder"``. + + """ if strategy == "preorder": yield self for child in self.children: @@ -168,7 +217,7 @@ class Tree(Datastructure): >>> branch = TreeNode('branch') >>> leaf1 = TreeNode('leaf1') >>> leaf2 = TreeNode('leaf2') - >>> tree.add_root(root) + >>> tree.add(root) >>> root.add(branch) >>> branch.add(leaf1) >>> branch.add(leaf2) @@ -210,39 +259,80 @@ def from_data(cls, data): def root(self): return self._root - def add_root(self, node): - """Add a root node to the tree.""" + def add(self, node, parent=None): + """ + Add a node to the tree. + + Parameters + ---------- + node : :class:`~compas.datastructures.TreeNode` + The node to add. + parent : :class:`~compas.datastructures.TreeNode`, optional + The parent node of the node to add. + Default is ``None``, in which case the node is added as a root node. + + Returns + ------- + None + + Raises + ------ + TypeError + If the node is not a :class:`~compas.datastructures.TreeNode` object. + If the supplied parent node is not a :class:`~compas.datastructures.TreeNode` object. + ValueError + If the node is already part of another tree. + If the supplied parent node is not part of this tree. + If the tree already has a root node, when trying to add a root node. + + """ if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") - if not node.is_root: - raise ValueError("The node is already part of another tree.") - self._root = node - node._tree = self - self._nodes.add(node) - - def add(self, node, parent): - """Add a node to the tree.""" - if not isinstance(node, TreeNode): - raise TypeError("The node is not a TreeNode object.") - if self.root is None: - raise ValueError("The tree has no root node, use add_root() first.") + + if node.parent: + raise ValueError("The node is already part of another tree, remove it from that tree first.") + + if parent is None: + # add the node as a root node + if self.root is not None: + raise ValueError("The tree already has a root node, remove it first.") + + self._root = node + node._tree = self + self._nodes.add(node) + else: + # add the node as a child of the parent node + if not isinstance(parent, TreeNode): + raise TypeError("The parent node is not a TreeNode object.") + + if parent.tree is not self: + raise ValueError("The parent node is not part of this tree.") + parent.add(node) @property def nodes(self): return self._nodes - def _remove_root(self): - """Remove the root node from the tree.""" - self._root._tree = None - self.nodes.remove(self._root) - self._root = None - def remove(self, node): - """Remove a node from the tree.""" + """ + Remove a node from the tree. + + Parameters + ---------- + node : :class:`~compas.datastructures.TreeNode` + The node to remove. + + Returns + ------- + None + + """ if node == self.root: - self._remove_root() + self._root._tree = None + self.nodes.remove(self._root) + self._root = None else: node.parent.remove(node) From 7666de49279feac5d22915136c194e46ed3feb54 Mon Sep 17 00:00:00 2001 From: Li Chen Date: Fri, 6 Oct 2023 11:55:39 +0200 Subject: [PATCH 09/14] simplify node tree links and add data schemas --- src/compas/datastructures/tree/tree.py | 53 ++++++++++++++------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 5563a17bfb7..1c0a67abc6c 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -26,8 +26,6 @@ class TreeNode(Data): The parent node of the tree node. children : set[:class:`~compas.datastructures.TreeNode`] The children of the tree node. - tree : :class:`~compas.datastructures.Tree` - The tree the node belongs to. is_root : bool True if the node is the root node of the tree. is_leaf : bool @@ -41,12 +39,22 @@ class TreeNode(Data): """ + DATASCHEMA = { + "type": "object", + "$recursiveAnchor": True, + "properties": { + "name": {"type": "string"}, + "attributes": {"type": "object"}, + "children": {"type": "array", "items": {"$recursiveRef": "#"}}, + }, + "required": ["name", "attributes", "children"], + } + def __init__(self, name=None, attributes=None): - self.name = name + super(TreeNode, self).__init__(name=name) self.attributes = attributes or {} self._parent = None self._children = set() - self._tree = None def __repr__(self): return "".format(self.name) @@ -71,10 +79,6 @@ def parent(self): def children(self): return self._children - @property - def tree(self): - return self._tree - @property def data(self): return { @@ -113,9 +117,6 @@ def add(self, node): raise TypeError("The node is not a TreeNode object.") self._children.add(node) node._parent = self - node._tree = self.tree - if self.tree: - self.tree.nodes.add(node) def remove(self, node): """ @@ -133,9 +134,6 @@ def remove(self, node): """ self._children.remove(node) node._parent = None - node._tree = None - if self.tree: - self.tree.nodes.remove(node) @property def ancestors(self): @@ -204,7 +202,7 @@ class Tree(Datastructure): User-defined attributes of the datastructure. root : :class:`~compas.datastructures.TreeNode` The root node of the tree. - nodes : set[:class:`~compas.datastructures.TreeNode`] + nodes : list[:class:`~compas.datastructures.TreeNode`] The nodes of the tree. leaves : generator[:class:`~compas.datastructures.TreeNode`] A generator of the leaves of the tree. @@ -231,14 +229,21 @@ class Tree(Datastructure): """ - JSONSCHEMA = {} + DATASCHEMA = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "root": TreeNode.DATASCHEMA, + "attributes": {"type": "object"}, + }, + "required": ["name", "root", "attributes"], + } def __init__(self, name=None, attributes=None): super(Tree, self).__init__() self.name = name self.attributes = attributes or {} self._root = None - self._nodes = set() @property def data(self): @@ -252,7 +257,7 @@ def data(self): def from_data(cls, data): tree = cls(data["name"], data["attributes"]) root = TreeNode.from_data(data["root"]) - tree.add_root(root) + tree.add(root) return tree @property @@ -298,8 +303,6 @@ def add(self, node, parent=None): raise ValueError("The tree already has a root node, remove it first.") self._root = node - node._tree = self - self._nodes.add(node) else: # add the node as a child of the parent node @@ -313,7 +316,11 @@ def add(self, node, parent=None): @property def nodes(self): - return self._nodes + if self.root: + for node in self.root.traverse(): + yield node + else: + yield iter([]) def remove(self, node): """ @@ -330,8 +337,6 @@ def remove(self, node): """ if node == self.root: - self._root._tree = None - self.nodes.remove(self._root) self._root = None else: node.parent.remove(node) @@ -343,7 +348,7 @@ def leaves(self): yield node def __repr__(self): - return "".format(len(self.nodes)) + return "".format(len(list(self.nodes))) def print(self): """Print the spatial hierarchy of the tree.""" From 9928394a0c802ab810820456614131f5dca53958 Mon Sep 17 00:00:00 2001 From: Li Chen Date: Fri, 6 Oct 2023 12:21:38 +0200 Subject: [PATCH 10/14] make tree link dynamic --- src/compas/datastructures/tree/tree.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 1c0a67abc6c..b99ecd47baf 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -26,6 +26,8 @@ class TreeNode(Data): The parent node of the tree node. children : set[:class:`~compas.datastructures.TreeNode`] The children of the tree node. + tree : :class:`~compas.datastructures.Tree` + The tree to which the node belongs. is_root : bool True if the node is the root node of the tree. is_leaf : bool @@ -55,6 +57,7 @@ def __init__(self, name=None, attributes=None): self.attributes = attributes or {} self._parent = None self._children = set() + self._tree = None def __repr__(self): return "".format(self.name) @@ -79,6 +82,13 @@ def parent(self): def children(self): return self._children + @property + def tree(self): + if self.is_root: + return self._tree + else: + return self.parent.tree + @property def data(self): return { @@ -303,6 +313,7 @@ def add(self, node, parent=None): raise ValueError("The tree already has a root node, remove it first.") self._root = node + node._tree = self else: # add the node as a child of the parent node @@ -338,6 +349,7 @@ def remove(self, node): """ if node == self.root: self._root = None + node._tree = None else: node.parent.remove(node) From 7c12d19e2c140fceb15f40eab21a715886d91863 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 9 Oct 2023 13:02:40 +0200 Subject: [PATCH 11/14] add tests --- src/compas/datastructures/tree/tree.py | 134 ++++++++++++++---- tests/compas/datastructures/test_tree.py | 170 +++++++++++++++++++++++ 2 files changed, 278 insertions(+), 26 deletions(-) create mode 100644 tests/compas/datastructures/test_tree.py diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index b99ecd47baf..7aaff506f2f 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -24,7 +24,7 @@ class TreeNode(Data): User-defined attributes of the datastructure. parent : :class:`~compas.datastructures.TreeNode` The parent node of the tree node. - children : set[:class:`~compas.datastructures.TreeNode`] + children : list[:class:`~compas.datastructures.TreeNode`] The children of the tree node. tree : :class:`~compas.datastructures.Tree` The tree to which the node belongs. @@ -37,7 +37,7 @@ class TreeNode(Data): acestors : generator[:class:`~compas.datastructures.TreeNode`] A generator of the acestors of the tree node. descendants : generator[:class:`~compas.datastructures.TreeNode`] - A generator of the descendants of the tree node. + A generator of the descendants of the tree node, using a depth-first preorder traversal. """ @@ -56,7 +56,7 @@ def __init__(self, name=None, attributes=None): super(TreeNode, self).__init__(name=name) self.attributes = attributes or {} self._parent = None - self._children = set() + self._children = [] self._tree = None def __repr__(self): @@ -125,7 +125,8 @@ def add(self, node): """ if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") - self._children.add(node) + if not node in self._children: + self._children.append(node) node._parent = self def remove(self, node): @@ -159,14 +160,18 @@ def descendants(self): for descendant in child.descendants: yield descendant - def traverse(self, strategy="preorder"): + def traverse(self, strategy="depthfirst", order="preorder"): """ Traverse the tree from this node. Parameters ---------- - strategy : str, optional - The traversal strategy. Options are ``"preorder"`` and ``"postorder"``. + strategy : {"depthfirst", "breadthfirst"}, optional + The traversal strategy. + Default is ``"depthfirst"``. + + order : {"preorder", "postorder"}, optional + The traversal order. This parameter is only used for depth-first traversal. Default is ``"preorder"``. Yields @@ -177,25 +182,35 @@ def traverse(self, strategy="preorder"): Raises ------ ValueError - If the strategy is not ``"preorder"`` or ``"postorder"``. + If the strategy is not ``"depthfirst"`` or ``"breadthfirst"``. + If the order is not ``"preorder"`` or ``"postorder"``. """ - if strategy == "preorder": - yield self - for child in self.children: - for node in child.traverse(strategy): - yield node - elif strategy == "postorder": - for child in self.children: - for node in child.traverse(strategy): - yield node - yield self + if strategy == "depthfirst": + if order == "preorder": + yield self + for child in self.children: + for node in child.traverse(strategy, order): + yield node + elif order == "postorder": + for child in self.children: + for node in child.traverse(strategy, order): + yield node + yield self + else: + raise ValueError("Unknown traversal order: {}".format(order)) + elif strategy == "breadthfirst": + queue = [self] + while queue: + node = queue.pop(0) + yield node + queue.extend(node.children) else: raise ValueError("Unknown traversal strategy: {}".format(strategy)) class Tree(Datastructure): - """A tree data structure. + """A hierarchical data structure that organizes elements into parent-child relationships. The tree starts from a unique root node, and every node (excluding the root) has exactly one parent. Parameters ---------- @@ -212,7 +227,7 @@ class Tree(Datastructure): User-defined attributes of the datastructure. root : :class:`~compas.datastructures.TreeNode` The root node of the tree. - nodes : list[:class:`~compas.datastructures.TreeNode`] + nodes : generator[:class:`~compas.datastructures.TreeNode`] The nodes of the tree. leaves : generator[:class:`~compas.datastructures.TreeNode`] A generator of the leaves of the tree. @@ -250,9 +265,8 @@ class Tree(Datastructure): } def __init__(self, name=None, attributes=None): - super(Tree, self).__init__() - self.name = name - self.attributes = attributes or {} + super(Tree, self).__init__(name=name) + self.attributes.update(attributes or {}) self._root = None @property @@ -305,7 +319,7 @@ def add(self, node, parent=None): raise TypeError("The node is not a TreeNode object.") if node.parent: - raise ValueError("The node is already part of another tree, remove it from that tree first.") + raise ValueError("The node already has a parent, remove it from that parent first.") if parent is None: # add the node as a root node @@ -330,8 +344,6 @@ def nodes(self): if self.root: for node in self.root.traverse(): yield node - else: - yield iter([]) def remove(self, node): """ @@ -359,6 +371,76 @@ def leaves(self): if node.is_leaf: yield node + def traverse(self, strategy="depthfirst", order="preorder"): + """ + Traverse the tree from the root node. + + Parameters + ---------- + strategy : {"depthfirst", "breadthfirst"}, optional + The traversal strategy. + Default is ``"depthfirst"``. + + order : {"preorder", "postorder"}, optional + The traversal order. This parameter is only used for depth-first traversal. + Default is ``"preorder"``. + + Yields + ------ + :class:`~compas.datastructures.TreeNode` + The next node in the traversal. + + Raises + ------ + ValueError + If the strategy is not ``"depthfirst"`` or ``"breadthfirst"``. + If the order is not ``"preorder"`` or ``"postorder"``. + + """ + if self.root: + for node in self.root.traverse(strategy=strategy, order=order): + yield node + + def get_node_by_name(self, name): + """ + Get a node by its name. + + Parameters + ---------- + name : str + The name of the node. + + Returns + ------- + :class:`~compas.datastructures.TreeNode` + The node. + + """ + for node in self.nodes: + if node.name == name: + return node + + def get_nodes_by_name(self, name): + """ + Get all nodes by their name. + + Parameters + ---------- + name : str + The name of the node. + + Returns + ------- + list[:class:`~compas.datastructures.TreeNode`] + The nodes. + + """ + nodes = [] + for node in self.nodes: + if node.name == name: + nodes.append(node) + return nodes + def __repr__(self): return "".format(len(list(self.nodes))) diff --git a/tests/compas/datastructures/test_tree.py b/tests/compas/datastructures/test_tree.py new file mode 100644 index 00000000000..b7709d23bf4 --- /dev/null +++ b/tests/compas/datastructures/test_tree.py @@ -0,0 +1,170 @@ +import pytest + +from compas.datastructures import Tree, TreeNode +from compas.data import json_dumps, json_loads +import json + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def simple_tree(): + # A basic fixture for a simple tree + root = TreeNode(name="root") + branch1 = TreeNode(name="branch1") + branch2 = TreeNode(name="branch2") + leaf1_1 = TreeNode(name="leaf1_1") + leaf1_2 = TreeNode(name="leaf1_2") + leaf2_1 = TreeNode(name="leaf2_1") + leaf2_2 = TreeNode(name="leaf2_2") + + tree = Tree() + tree.add(root) + tree.add(branch1, parent=root) + tree.add(branch2, parent=root) + tree.add(leaf1_1, parent=branch1) + tree.add(leaf1_2, parent=branch1) + tree.add(leaf2_1, parent=branch2) + tree.add(leaf2_2, parent=branch2) + return tree + + +# ============================================================================= +# Basics +# ============================================================================= + + +def test_treenode_initialization(): + node = TreeNode(name="test") + assert node.name == "test" + assert node.parent is None + assert node.tree is None + assert len(node.children) == 0 + + +def test_tree_initialization(): + tree = Tree(name="test") + assert tree.name == "test" + assert tree.root is None + + +# ============================================================================= +# TreeNode Properties +# ============================================================================= + + +def test_treenode_properties(simple_tree): + root = simple_tree.root + branch1, branch2 = list(root.children) + leaf1_1, leaf1_2 = list(branch1.children) + leaf2_1, leaf2_2 = list(branch2.children) + + assert root.is_root == True + assert root.is_leaf == False + assert root.is_branch == False + + assert branch1.is_root == False + assert branch1.is_leaf == False + assert branch1.is_branch == True + + assert branch2.is_root == False + assert branch2.is_leaf == False + assert branch2.is_branch == True + + assert leaf1_1.is_root == False + assert leaf1_1.is_leaf == True + assert leaf1_1.is_branch == False + + assert leaf1_2.is_root == False + assert leaf1_2.is_leaf == True + assert leaf1_2.is_branch == False + + assert leaf2_1.is_root == False + assert leaf2_1.is_leaf == True + assert leaf2_1.is_branch == False + + assert leaf2_2.is_root == False + assert leaf2_2.is_leaf == True + assert leaf2_2.is_branch == False + + +# ============================================================================= +# Tree Properties +# ============================================================================= + + +def test_tree_properties(simple_tree): + nodes = list(simple_tree.nodes) + leaves = list(simple_tree.leaves) + + assert len(nodes) == 7 + assert len(leaves) == 4 + + +# ============================================================================= +# Tree Traversal +# ============================================================================= + + +def test_tree_traversal(simple_tree): + nodes = [node.name for node in simple_tree.traverse(strategy="depthfirst", order="preorder")] + assert nodes == ["root", "branch1", "leaf1_1", "leaf1_2", "branch2", "leaf2_1", "leaf2_2"] + + nodes = [node.name for node in simple_tree.traverse(strategy="depthfirst", order="postorder")] + assert nodes == ["leaf1_1", "leaf1_2", "branch1", "leaf2_1", "leaf2_2", "branch2", "root"] + + nodes = [node.name for node in simple_tree.traverse(strategy="breadthfirst")] + assert nodes == ["root", "branch1", "branch2", "leaf1_1", "leaf1_2", "leaf2_1", "leaf2_2"] + + +# ============================================================================= +# Tree Manipulation +# ============================================================================= + + +def test_tree_add_node(simple_tree): + branch2 = simple_tree.get_node_by_name("branch2") + branch2.add(TreeNode(name="test")) + + assert len(list(branch2.children)) == 3 + assert len(list(simple_tree.nodes)) == 8 + + +def test_tree_remove_node(simple_tree): + branch2 = simple_tree.get_node_by_name("branch2") + leaf2_1 = simple_tree.get_node_by_name("leaf2_1") + branch2.remove(leaf2_1) + + assert len(list(branch2.children)) == 1 + assert len(list(simple_tree.nodes)) == 6 + + root = simple_tree.root + branch1 = simple_tree.get_node_by_name("branch1") + root.remove(branch1) + + assert len(list(root.children)) == 1 + assert len(list(simple_tree.nodes)) == 3 + + +# ============================================================================= +# Tree Serialization +# ============================================================================= + + +def test_tree_serialization(simple_tree): + serialized = json_dumps(simple_tree) + deserialized = json_loads(serialized) + assert simple_tree.data == deserialized.data + + test_tree_properties(deserialized) + test_tree_traversal(deserialized) + test_tree_add_node(deserialized) + test_tree_remove_node(json_loads(serialized)) + + +def test_data_validation(simple_tree): + serialized = json_dumps(simple_tree) + data = json.loads(serialized)["data"] + assert Tree.validate_data(data) From a0d2b4f56f067378b7257342579ede4981b3c2de Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 9 Oct 2023 13:06:49 +0200 Subject: [PATCH 12/14] lint --- src/compas/datastructures/tree/tree.py | 2 +- tests/compas/datastructures/test_tree.py | 42 ++++++++++++------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 7aaff506f2f..cb11a863d2e 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -125,7 +125,7 @@ def add(self, node): """ if not isinstance(node, TreeNode): raise TypeError("The node is not a TreeNode object.") - if not node in self._children: + if node not in self._children: self._children.append(node) node._parent = self diff --git a/tests/compas/datastructures/test_tree.py b/tests/compas/datastructures/test_tree.py index b7709d23bf4..29fb8c2910e 100644 --- a/tests/compas/datastructures/test_tree.py +++ b/tests/compas/datastructures/test_tree.py @@ -61,33 +61,33 @@ def test_treenode_properties(simple_tree): leaf1_1, leaf1_2 = list(branch1.children) leaf2_1, leaf2_2 = list(branch2.children) - assert root.is_root == True - assert root.is_leaf == False - assert root.is_branch == False + assert root.is_root is True + assert root.is_leaf is False + assert root.is_branch is False - assert branch1.is_root == False - assert branch1.is_leaf == False - assert branch1.is_branch == True + assert branch1.is_root is False + assert branch1.is_leaf is False + assert branch1.is_branch is True - assert branch2.is_root == False - assert branch2.is_leaf == False - assert branch2.is_branch == True + assert branch2.is_root is False + assert branch2.is_leaf is False + assert branch2.is_branch is True - assert leaf1_1.is_root == False - assert leaf1_1.is_leaf == True - assert leaf1_1.is_branch == False + assert leaf1_1.is_root is False + assert leaf1_1.is_leaf is True + assert leaf1_1.is_branch is False - assert leaf1_2.is_root == False - assert leaf1_2.is_leaf == True - assert leaf1_2.is_branch == False + assert leaf1_2.is_root is False + assert leaf1_2.is_leaf is True + assert leaf1_2.is_branch is False - assert leaf2_1.is_root == False - assert leaf2_1.is_leaf == True - assert leaf2_1.is_branch == False + assert leaf2_1.is_root is False + assert leaf2_1.is_leaf is True + assert leaf2_1.is_branch is False - assert leaf2_2.is_root == False - assert leaf2_2.is_leaf == True - assert leaf2_2.is_branch == False + assert leaf2_2.is_root is False + assert leaf2_2.is_leaf is True + assert leaf2_2.is_branch is False # ============================================================================= From c0ba4de4e8d41de724f6a15db8056761081a6273 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 9 Oct 2023 13:32:30 +0200 Subject: [PATCH 13/14] skip data validate for ipy --- src/compas/datastructures/tree/tree.py | 4 ---- tests/compas/datastructures/test_tree.py | 11 +++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index cb11a863d2e..9d78ae05a1f 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -168,11 +168,9 @@ def traverse(self, strategy="depthfirst", order="preorder"): ---------- strategy : {"depthfirst", "breadthfirst"}, optional The traversal strategy. - Default is ``"depthfirst"``. order : {"preorder", "postorder"}, optional The traversal order. This parameter is only used for depth-first traversal. - Default is ``"preorder"``. Yields ------ @@ -379,11 +377,9 @@ def traverse(self, strategy="depthfirst", order="preorder"): ---------- strategy : {"depthfirst", "breadthfirst"}, optional The traversal strategy. - Default is ``"depthfirst"``. order : {"preorder", "postorder"}, optional The traversal order. This parameter is only used for depth-first traversal. - Default is ``"preorder"``. Yields ------ diff --git a/tests/compas/datastructures/test_tree.py b/tests/compas/datastructures/test_tree.py index 29fb8c2910e..384a9091f12 100644 --- a/tests/compas/datastructures/test_tree.py +++ b/tests/compas/datastructures/test_tree.py @@ -1,8 +1,9 @@ import pytest +import compas +import json from compas.datastructures import Tree, TreeNode from compas.data import json_dumps, json_loads -import json # ============================================================================= # Fixtures @@ -163,8 +164,6 @@ def test_tree_serialization(simple_tree): test_tree_add_node(deserialized) test_tree_remove_node(json_loads(serialized)) - -def test_data_validation(simple_tree): - serialized = json_dumps(simple_tree) - data = json.loads(serialized)["data"] - assert Tree.validate_data(data) + if not compas.IPY: + data = json.loads(serialized)["data"] + assert Tree.validate_data(data) From f3ef02bb12e3652a0d9bd519ea0013b93c964c19 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 9 Oct 2023 15:29:42 +0200 Subject: [PATCH 14/14] no spaces --- src/compas/datastructures/tree/tree.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/compas/datastructures/tree/tree.py b/src/compas/datastructures/tree/tree.py index 9d78ae05a1f..7d55c7fcb94 100644 --- a/src/compas/datastructures/tree/tree.py +++ b/src/compas/datastructures/tree/tree.py @@ -168,7 +168,6 @@ def traverse(self, strategy="depthfirst", order="preorder"): ---------- strategy : {"depthfirst", "breadthfirst"}, optional The traversal strategy. - order : {"preorder", "postorder"}, optional The traversal order. This parameter is only used for depth-first traversal. @@ -377,7 +376,6 @@ def traverse(self, strategy="depthfirst", order="preorder"): ---------- strategy : {"depthfirst", "breadthfirst"}, optional The traversal strategy. - order : {"preorder", "postorder"}, optional The traversal order. This parameter is only used for depth-first traversal.