diff --git a/anytree/config.py b/anytree/config.py new file mode 100644 index 0000000..55a163c --- /dev/null +++ b/anytree/config.py @@ -0,0 +1,6 @@ +"""Central Configuration.""" + +import os + +# Global Option which enables all internal assertions. +ASSERTIONS = bool(int(os.environ.get("ANYTREE_ASSERTIONS", 0))) diff --git a/anytree/importer/dictimporter.py b/anytree/importer/dictimporter.py index 35e927d..e0adb8d 100644 --- a/anytree/importer/dictimporter.py +++ b/anytree/importer/dictimporter.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from anytree import AnyNode +from ..config import ASSERTIONS + class DictImporter: """ @@ -38,8 +40,9 @@ def import_(self, data): return self.__import(data) def __import(self, data, parent=None): - assert isinstance(data, dict) - assert "parent" not in data + if ASSERTIONS: # pragma: no branch + assert isinstance(data, dict) + assert "parent" not in data attrs = dict(data) children = attrs.pop("children", []) node = self.nodecls(parent=parent, **attrs) diff --git a/anytree/iterators/zigzaggroupiter.py b/anytree/iterators/zigzaggroupiter.py index a9ce59b..1f53254 100644 --- a/anytree/iterators/zigzaggroupiter.py +++ b/anytree/iterators/zigzaggroupiter.py @@ -1,3 +1,4 @@ +from ..config import ASSERTIONS from .abstractiter import AbstractIter from .levelordergroupiter import LevelOrderGroupIter @@ -46,7 +47,8 @@ class ZigZagGroupIter(AbstractIter): @staticmethod def _iter(children, filter_, stop, maxlevel): if children: - assert len(children) == 1 + if ASSERTIONS: # pragma: no branch + assert len(children) == 1 _iter = LevelOrderGroupIter(children[0], filter_, stop, maxlevel) while True: try: diff --git a/anytree/node/nodemixin.py b/anytree/node/nodemixin.py index 37002b1..9224417 100644 --- a/anytree/node/nodemixin.py +++ b/anytree/node/nodemixin.py @@ -4,6 +4,7 @@ from anytree.iterators import PreOrderIter +from ..config import ASSERTIONS from .exceptions import LoopError, TreeError @@ -146,7 +147,8 @@ def __detach(self, parent): if parent is not None: self._pre_detach(parent) parentchildren = parent.__children_or_empty - assert any(child is self for child in parentchildren), "Tree is corrupt." # pragma: no cover + if ASSERTIONS: # pragma: no branch + assert any(child is self for child in parentchildren), "Tree is corrupt." # pragma: no cover # ATOMIC START parent.__children = [child for child in parentchildren if child is not self] self.__parent = None @@ -158,7 +160,8 @@ def __attach(self, parent): if parent is not None: self._pre_attach(parent) parentchildren = parent.__children_or_empty - assert not any(child is self for child in parentchildren), "Tree is corrupt." # pragma: no cover + if ASSERTIONS: # pragma: no branch + assert not any(child is self for child in parentchildren), "Tree is corrupt." # pragma: no cover # ATOMIC START parentchildren.append(self) self.__parent = parent @@ -249,7 +252,8 @@ def children(self, children): for child in children: child.parent = self self._post_attach_children(children) - assert len(self.children) == len(children) + if ASSERTIONS: # pragma: no branch + assert len(self.children) == len(children) except Exception: self.children = old_children raise @@ -261,7 +265,8 @@ def children(self): self._pre_detach_children(children) for child in self.children: child.parent = None - assert len(self.children) == 0 + if ASSERTIONS: # pragma: no branch + assert len(self.children) == 0 self._post_detach_children(children) def _pre_detach_children(self, children): diff --git a/anytree/render.py b/anytree/render.py index c777df9..155330e 100644 --- a/anytree/render.py +++ b/anytree/render.py @@ -13,6 +13,8 @@ import six +from .config import ASSERTIONS + Row = collections.namedtuple("Row", ("pre", "fill", "node")) @@ -34,11 +36,12 @@ def __init__(self, vertical, cont, end): self.vertical = vertical self.cont = cont self.end = end - assert len(cont) == len(vertical) == len(end), "'%s', '%s' and '%s' need to have equal length" % ( - vertical, - cont, - end, - ) + if ASSERTIONS: # pragma: no branch + assert len(cont) == len(vertical) == len(end), "'%s', '%s' and '%s' need to have equal length" % ( + vertical, + cont, + end, + ) @property def empty(self): diff --git a/anytree/resolver.py b/anytree/resolver.py index a6b222b..0fe8fea 100644 --- a/anytree/resolver.py +++ b/anytree/resolver.py @@ -5,6 +5,8 @@ import re +from .config import ASSERTIONS + _MAXCACHE = 20 @@ -203,7 +205,8 @@ def __start(self, node, path, cmp_): return node, parts def __glob(self, node, parts): - assert node is not None + if ASSERTIONS: # pragma: no branch + assert node is not None nodes = [] if parts: name = parts[0] diff --git a/anytree/walker.py b/anytree/walker.py index da7d91f..88f7daa 100644 --- a/anytree/walker.py +++ b/anytree/walker.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from .config import ASSERTIONS + class Walker: """Walk from one node to another.""" @@ -70,7 +72,8 @@ def walk(start, end): raise WalkError(msg) # common common = Walker.__calc_common(startpath, endpath) - assert common[0] is start.root + if ASSERTIONS: # pragma: no branch + assert common[0] is start.root len_common = len(common) # upwards if start is common[-1]: diff --git a/docs/api/anytree.node.rst b/docs/api/anytree.node.rst index 26ddbb0..ad3f93f 100644 --- a/docs/api/anytree.node.rst +++ b/docs/api/anytree.node.rst @@ -8,6 +8,7 @@ Node Classes .. automodule:: anytree.node.node .. automodule:: anytree.node.nodemixin + :private-members: .. automodule:: anytree.node.symlinknode diff --git a/docs/intro.rst b/docs/intro.rst index 4ff405a..d5c42d3 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -115,8 +115,8 @@ Detach/Attach Protocol ---------------------- A node class implementation might implement the notification slots -:any:`_pre_detach(parent)`, :any:`_post_detach(parent)`, -:any:`_pre_attach(parent)`, :any:`_post_attach(parent)`. +``_pre_detach(parent)``, ``_post_detach(parent)``, +``_pre_attach(parent)``, ``_post_attach(parent)``. These methods are *protected* methods, intended to be overwritten by child classes of :any:`NodeMixin`/:any:`Node`. @@ -163,9 +163,9 @@ _post_detach NotifiedNode('/b') .. important:: - An exception raised by :any:`_pre_detach(parent)` and :any:`_pre_attach(parent)` will **prevent** the tree structure to be updated. + An exception raised by ``_pre_detach(parent)`` and ``_pre_attach(parent)`` will **prevent** the tree structure to be updated. The node keeps the old state. - An exception raised by :any:`_post_detach(parent)` and :any:`_post_attach(parent)` does **not rollback** the tree structure modification. + An exception raised by ``_post_detach(parent)`` and ``_post_attach(parent)`` does **not rollback** the tree structure modification. Custom Separator diff --git a/docs/tricks.rst b/docs/tricks.rst index 748864a..40562df 100644 --- a/docs/tricks.rst +++ b/docs/tricks.rst @@ -8,3 +8,4 @@ Tricks tricks/yaml tricks/multidim tricks/weightededges + tricks/consistencychecks diff --git a/docs/tricks/consistencychecks.rst b/docs/tricks/consistencychecks.rst new file mode 100644 index 0000000..692650f --- /dev/null +++ b/docs/tricks/consistencychecks.rst @@ -0,0 +1,13 @@ +Consistency Checks vs. Speed +============================ + +Anytree can run some *costly* internal consistency checks. +With version 2.9.2 these got disabled by default. +In case of any concerns about the internal data consistency or just for safety, either + +* set the environment variable ``ANYTREE_ASSERTIONS=1``, or +* add the following lines to your code: + +>>> import anytree +>>> anytree.config.ASSERTIONS = True + diff --git a/pyproject.toml b/pyproject.toml index 6f20245..05af691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ line_length = 120 exclude_lines = [ 'return NotImplemented', 'raise NotImplementedError()', - 'pragma: no cover' + 'pragma: no cover', ] @@ -102,6 +102,9 @@ basepython = python3 [testenv] allowlist_externals = * +setenv = + ANYTREE_ASSERTIONS=1 + commands = poetry install --with=test --with=doc poetry run black .