diff --git a/CHANGELOG.md b/CHANGELOG.md index 61be315b..53ea5570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,18 +5,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [0.16.2] - 2024-02-06 ### Added - Misc: Documentation plugin Termynal for code animation. - Misc: Usage of `docstr-coverage`. - Misc: Docstrings for nested functions to pass `docstr-coverage`. ### Changed +- [#185] BaseNode: Make assertion checks optional. - Misc: Documentation CSS for h1 display for windows compatibility, modify the related links on main page. -## [0.16.1] - 2023-01-29 +## [0.16.1] - 2024-01-29 ### Fixed - Misc: Compatibility of mkdocs with readthedocs. -## [0.16.0] - 2023-01-28 +## [0.16.0] - 2024-01-28 ### Added - Misc: Documentation using mkdocs. ### Changed @@ -25,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Misc: Docstring bullet point alignment, images compatibility with markdown. -## [0.15.7] - 2023-01-26 +## [0.15.7] - 2024-01-26 ### Added - Misc: Sphinx documentation to support mermaid markdown images, reflect CHANGELOG section, add more emojis. ### Changed @@ -35,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Tree Exporter: `hprint_tree` and `hyield_tree` to be compatible with `BinaryNode` where child nodes can be None type. -## [0.15.6] - 2023-01-20 +## [0.15.6] - 2024-01-20 ### Added - DAGNode: Able to access and delete node children via name with square bracket accessor with `__getitem__` and `__delitem__` magic methods. - DAGNode: Able to delete all children for a node. @@ -47,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Misc: Documentation enhancement to split README into multiple files. - Misc: New Sphinx documentation theme. -## [0.15.5] - 2023-01-17 +## [0.15.5] - 2024-01-17 ### Changed - Misc: Neater handling of strings for tests. - Misc: Better examples for merging trees and weighted trees in Sphinx documentation. @@ -492,7 +495,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Utility Iterator: Tree traversal methods. - Workflow To Do App: Tree use case with to-do list implementation. -[Unreleased]: https://github.com/kayjan/bigtree/compare/0.16.1...HEAD +[Unreleased]: https://github.com/kayjan/bigtree/compare/0.16.2...HEAD +[0.16.2]: https://github.com/kayjan/bigtree/compare/0.16.1...0.16.2 [0.16.1]: https://github.com/kayjan/bigtree/compare/0.16.0...0.16.1 [0.16.0]: https://github.com/kayjan/bigtree/compare/0.15.7...0.16.0 [0.15.7]: https://github.com/kayjan/bigtree/compare/0.15.6...0.15.7 diff --git a/bigtree/__init__.py b/bigtree/__init__.py index 463a4607..455ffc14 100644 --- a/bigtree/__init__.py +++ b/bigtree/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.16.1" +__version__ = "0.16.2" from bigtree.binarytree.construct import list_to_binarytree from bigtree.dag.construct import dataframe_to_dag, dict_to_dag, list_to_dag @@ -75,4 +75,4 @@ from bigtree.workflows.app_calendar import Calendar from bigtree.workflows.app_todo import AppToDo -sphinx_versions = ["latest", "0.16.1", "0.15.7", "0.14.8"] +sphinx_versions = ["latest", "0.16.2", "0.15.7", "0.14.8"] diff --git a/bigtree/globals.py b/bigtree/globals.py new file mode 100644 index 00000000..52e79771 --- /dev/null +++ b/bigtree/globals.py @@ -0,0 +1,3 @@ +import os + +ASSERTIONS: bool = bool(os.environ.get("BIGTREE_CONF_ASSERTIONS", True)) diff --git a/bigtree/node/basenode.py b/bigtree/node/basenode.py index ab0fca39..5f4c4207 100644 --- a/bigtree/node/basenode.py +++ b/bigtree/node/basenode.py @@ -3,6 +3,7 @@ import copy from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Tuple, TypeVar +from bigtree.globals import ASSERTIONS from bigtree.utils.exceptions import CorruptedTreeError, LoopError, TreeError from bigtree.utils.iterators import preorder_iter @@ -181,8 +182,9 @@ def parent(self: T, new_parent: T) -> None: Args: new_parent (Self): parent node """ - self.__check_parent_type(new_parent) - self.__check_parent_loop(new_parent) + if ASSERTIONS: + self.__check_parent_type(new_parent) + self.__check_parent_loop(new_parent) current_parent = self.parent current_child_idx = None @@ -325,8 +327,9 @@ def children(self: T, new_children: List[T] | Tuple[T] | Set[T]) -> None: Args: new_children (List[Self]): child node """ - self.__check_children_type(new_children) - self.__check_children_loop(new_children) + if ASSERTIONS: + self.__check_children_type(new_children) + self.__check_children_loop(new_children) new_children = list(new_children) current_new_children = { diff --git a/bigtree/node/binarynode.py b/bigtree/node/binarynode.py index 49f5d813..4f796f8f 100644 --- a/bigtree/node/binarynode.py +++ b/bigtree/node/binarynode.py @@ -2,6 +2,7 @@ from typing import Any, List, Optional, Tuple, TypeVar, Union +from bigtree.globals import ASSERTIONS from bigtree.node.node import Node from bigtree.utils.exceptions import CorruptedTreeError, LoopError, TreeError @@ -165,8 +166,9 @@ def parent(self: T, new_parent: Optional[T]) -> None: Args: new_parent (Optional[Self]): parent node """ - self.__check_parent_type(new_parent) - self._BaseNode__check_parent_loop(new_parent) # type: ignore + if ASSERTIONS: + self.__check_parent_type(new_parent) + self._BaseNode__check_parent_loop(new_parent) # type: ignore current_parent = self.parent current_child_idx = None @@ -294,7 +296,8 @@ def children(self: T, _new_children: List[Optional[T]]) -> None: """ self._BaseNode__check_children_type(_new_children) # type: ignore new_children = self.__check_children_type(_new_children) - self.__check_children_loop(new_children) + if ASSERTIONS: + self.__check_children_loop(new_children) current_new_children = { new_child: ( diff --git a/bigtree/node/dagnode.py b/bigtree/node/dagnode.py index 1d616542..a72aad1e 100644 --- a/bigtree/node/dagnode.py +++ b/bigtree/node/dagnode.py @@ -3,6 +3,7 @@ import copy from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, TypeVar +from bigtree.globals import ASSERTIONS from bigtree.utils.exceptions import LoopError, TreeError from bigtree.utils.iterators import preorder_iter @@ -208,8 +209,9 @@ def parents(self: T, new_parents: List[T]) -> None: Args: new_parents (List[Self]): parent nodes """ - self.__check_parent_type(new_parents) - self.__check_parent_loop(new_parents) + if ASSERTIONS: + self.__check_parent_type(new_parents) + self.__check_parent_loop(new_parents) current_parents = self.__parents.copy() @@ -306,8 +308,9 @@ def children(self: T, new_children: Iterable[T]) -> None: Args: new_children (Iterable[Self]): child node """ - self.__check_children_type(new_children) - self.__check_children_loop(new_children) + if ASSERTIONS: + self.__check_children_type(new_children) + self.__check_children_loop(new_children) current_children = list(self.children) diff --git a/docs/others/remove_checks.md b/docs/others/remove_checks.md new file mode 100644 index 00000000..29cb45e0 --- /dev/null +++ b/docs/others/remove_checks.md @@ -0,0 +1,20 @@ +# Remove Tree Checks + +!!! note + + Available from version 0.16.2 onwards + +When constructing trees, there are a few checks done that slow down performance. +This slowness will be more apparent with very large trees. The checks are to + +- Check parent/children data type +- Check for loops (expensive for trees that are deep as it checks the ancestors of node) + +These checks are enabled by default. To turn off these checks, you can set environment variable before importing `bigtree`. + +```python +import os +os.environ["BIGTREE_CONF_ASSERTIONS"] = "" + +import bigtree +``` diff --git a/mkdocs.yml b/mkdocs.yml index f6b19d15..957edaed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ nav: - Others: - Tips and Tricks: - others/index.md + - others/remove_checks.md - others/list_dir.md - others/nodes.md - others/merging_trees.md diff --git a/tests/node/test_basenode_no_assertions.py b/tests/node/test_basenode_no_assertions.py new file mode 100644 index 00000000..c38ba3c2 --- /dev/null +++ b/tests/node/test_basenode_no_assertions.py @@ -0,0 +1,85 @@ +import unittest +from unittest.mock import patch + +import pytest + +from bigtree.node.basenode import BaseNode + + +@patch("bigtree.node.basenode.ASSERTIONS", "") +class TestBaseNodeNoAssertions(unittest.TestCase): + def setUp(self): + """ + Tree should have structure + a (age=90) + |-- b (age=65) + | |-- d (age=40) + | +-- e (age=35) + | |-- g (age=10) + | +-- h (age=6) + +-- c (age=60) + +-- f (age=38) + """ + self.a = BaseNode(name="a", age=90) + self.b = BaseNode(name="b", age=65) + self.c = BaseNode(name="c", age=60) + self.d = BaseNode(name="d", age=40) + self.e = BaseNode(name="e", age=35) + self.f = BaseNode(name="f", age=38) + self.g = BaseNode(name="g", age=10) + self.h = BaseNode(name="h", age=6) + + def tearDown(self): + self.a = None + self.b = None + self.c = None + self.d = None + self.e = None + self.f = None + self.g = None + self.h = None + + def test_set_children_none_parent_error(self): + # TypeError: 'NoneType' object is not iterable + children = None + with pytest.raises(TypeError): + self.h.children = children + + def test_set_parent_type_error(self): + # AttributeError: 'int' object has no attribute '_BaseNode__children' + parent = 1 + with pytest.raises(AttributeError): + self.a.parent = parent + + def test_set_parent_loop_error(self): + # No error without assertion + self.a.parent = self.a + + # No error without assertion + self.b.parent = self.a + self.c.parent = self.b + self.a.parent = self.c + + def test_set_children_type_error(self): + # AttributeError: 'int' object has no attribute '_BaseNode__children' + children = 1 + with pytest.raises(AttributeError): + self.a.children = [self.b, children] + + # AttributeError: 'NoneType' object has no attribute 'parent' + children = None + with pytest.raises(AttributeError): + self.a.children = [self.b, children] + + def test_set_children_loop_error(self): + # No error without assertion + self.a.children = [self.b, self.a] + + # No error without assertion + self.a.children = [self.b, self.c] + self.c.children = [self.d, self.e, self.f] + self.f.children = [self.a] + + def test_set_duplicate_children_error(self): + # No error without assertion + self.a.children = [self.b, self.b] diff --git a/tests/node/test_binarynode_no_assertions.py b/tests/node/test_binarynode_no_assertions.py new file mode 100644 index 00000000..0b4fe404 --- /dev/null +++ b/tests/node/test_binarynode_no_assertions.py @@ -0,0 +1,85 @@ +import unittest +from unittest.mock import patch + +import pytest + +from bigtree.node.basenode import BaseNode +from bigtree.node.binarynode import BinaryNode +from bigtree.node.node import Node +from bigtree.utils.exceptions import TreeError + + +@patch("bigtree.node.binarynode.ASSERTIONS", "") +class TestBinaryNodeNoAssertions(unittest.TestCase): + def setUp(self): + self.a = BinaryNode(1) + self.b = BinaryNode(2) + self.c = BinaryNode(3) + self.d = BinaryNode(4) + self.e = BinaryNode(5) + self.f = BinaryNode(6) + self.g = BinaryNode(7) + self.h = BinaryNode(8) + + def tearDown(self): + self.a = None + self.b = None + self.c = None + self.d = None + self.e = None + self.f = None + self.g = None + self.h = None + + def test_set_parent_type_error(self): + # AttributeError: 'int' object has no attribute '_BinaryNode__children' + parent = 1 + with pytest.raises(AttributeError): + self.a.parent = parent + + # AttributeError: 'BaseNode' object has no attribute '_BinaryNode__children' + parent = BaseNode() + with pytest.raises(AttributeError): + self.a.parent = parent + + # AttributeError: 'Node' object has no attribute '_BinaryNode__children' + parent = Node("a") + with pytest.raises(AttributeError): + self.a.parent = parent + + def test_set_parent_loop_error(self): + # No error without assertion + self.a.parent = self.a + + # No error without assertion + self.b.parent = self.a + self.c.parent = self.b + self.a.parent = self.c + + def test_set_children_type_error(self): + # AttributeError: 'int' object has no attribute 'parent' + children = 1 + with pytest.raises(AttributeError): + self.a.children = [self.b, children] + + # No error without assertion + children = BaseNode() + self.a.children = [children, None] + + # bigtree.utils.exceptions.TreeError: 'NoneType' object has no attribute '_BinaryNode__children' + children = Node("a") + with pytest.raises(TreeError): + self.a.children = [children, None] + + def test_set_children_loop_error(self): + # No error without assertion + self.a.children = [self.b, self.a] + + # No error without assertion + self.a.children = [self.b, self.c] + self.c.children = [self.d, self.e] + self.e.children = [self.a, self.f] + + def test_set_duplicate_children_error(self): + # No error without assertion + self.a.children = [self.b, self.b] diff --git a/tests/node/test_dagnode_no_assertions.py b/tests/node/test_dagnode_no_assertions.py new file mode 100644 index 00000000..80bb1bb0 --- /dev/null +++ b/tests/node/test_dagnode_no_assertions.py @@ -0,0 +1,125 @@ +import unittest +from unittest.mock import patch + +import pytest + +from bigtree.node.basenode import BaseNode +from bigtree.node.dagnode import DAGNode +from bigtree.node.node import Node + + +@patch("bigtree.node.dagnode.ASSERTIONS", "") +class TestDAGNodeNoAssertions(unittest.TestCase): + def setUp(self): + """ + Tree should have structure + a >> b + b >> c + b >> d + c >> e + c >> f + e >> f + f >> g + d >> e + """ + self.a = DAGNode(name="a", age=90) + self.b = DAGNode(name="b", age=65) + self.c = DAGNode(name="c", age=60) + self.d = DAGNode(name="d", age=40) + self.e = DAGNode(name="e", age=35) + self.f = DAGNode(name="f", age=38) + self.g = DAGNode(name="g", age=10) + self.h = DAGNode(name="h", age=6) + + self.nodes = [self.a, self.b, self.c, self.d, self.e, self.f, self.g] + + def tearDown(self): + self.a = None + self.b = None + self.c = None + self.d = None + self.e = None + self.f = None + self.g = None + self.h = None + + def test_set_parents_none_parent_error(self): + # TypeError: 'NoneType' object is not iterable + self.c.parents = [self.a] + parents = None + with pytest.raises(TypeError): + self.c.parents = parents + + def test_set_children_none_children_error(self): + # TypeError: 'NoneType' object is not iterable + children = None + with pytest.raises(TypeError): + self.g.children = children + + def test_set_parents_type_error(self): + # TypeError: 'int' object is not iterable + parents = 1 + with pytest.raises(TypeError): + self.a.parents = parents + + # AttributeError: 'int' object has no attribute '_DAGNode__children' + parent = 1 + with pytest.raises(AttributeError): + self.a.parents = [parent] + + # AttributeError: 'BaseNode' object has no attribute '_DAGNode__parents' + parent = BaseNode() + with pytest.raises(AttributeError): + self.a.parents = [parent] + + # AttributeError: 'Node' object has no attribute '_DAGNode__parents' + parent = Node("a") + with pytest.raises(AttributeError): + self.a.parents = [parent] + + def test_set_parents_loop_error(self): + # No error without assertion + self.a.parents = [self.a] + + # No error without assertion + self.b.parents = [self.a] + self.c.parents = [self.b] + self.a.parents = [self.c] + + def test_set_duplicate_parent_error(self): + # No error without assertion + self.a.parents = [self.b, self.b] + + def test_set_children_type_error(self): + # TypeError: 'int' object is not iterable + children = 1 + with pytest.raises(TypeError): + self.a.children = children + + # AttributeError: 'int' object has no attribute '_DAGNode__parents' + children = 1 + with pytest.raises(AttributeError): + self.a.children = [self.b, children] + + # AttributeError: 'BaseNode' object has no attribute '_DAGNode__parents' + children = BaseNode() + with pytest.raises(AttributeError): + self.a.children = [children] + + # AttributeError: 'Node' object has no attribute '_DAGNode__parents' + children = Node("a") + with pytest.raises(AttributeError): + self.a.children = [children] + + def test_set_children_loop_error(self): + # No error without assertion + self.a.children = [self.b, self.a] + + # No error without assertion + self.a.children = [self.b, self.c] + self.c.children = [self.d, self.e, self.f] + self.f.children = [self.a] + + def test_set_duplicate_children_error(self): + # No error without assertion + self.a.children = [self.b, self.b]