diff --git a/anytree/__init__.py b/anytree/__init__.py new file mode 100644 index 0000000..dfc4fb5 --- /dev/null +++ b/anytree/__init__.py @@ -0,0 +1,44 @@ +"""Powerful and Lightweight Python Tree Data Structure.""" + +__version__ = "2.12.1" +__author__ = "c0fec0de" +__author_email__ = "c0fec0de@gmail.com" +__description__ = """Powerful and Lightweight Python Tree Data Structure.""" +__url__ = "https://github.com/c0fec0de/anytree" + +# pylint: disable=useless-import-alias +from . import cachedsearch as cachedsearch # noqa: PLC0414 +from . import util as util # noqa: PLC0414 +from .iterators import LevelOrderGroupIter as LevelOrderGroupIter # noqa: PLC0414 +from .iterators import LevelOrderIter as LevelOrderIter # noqa: PLC0414 +from .iterators import PostOrderIter as PostOrderIter # noqa: PLC0414 +from .iterators import PreOrderIter as PreOrderIter # noqa: PLC0414 +from .iterators import ZigZagGroupIter as ZigZagGroupIter # noqa: PLC0414 +from .node import AnyNode as AnyNode # noqa: PLC0414 +from .node import LightNodeMixin as LightNodeMixin # noqa: PLC0414 +from .node import LoopError as LoopError # noqa: PLC0414 +from .node import Node as Node # noqa: PLC0414 +from .node import NodeMixin as NodeMixin # noqa: PLC0414 +from .node import SymlinkNode as SymlinkNode # noqa: PLC0414 +from .node import SymlinkNodeMixin as SymlinkNodeMixin # noqa: PLC0414 +from .node import TreeError as TreeError # noqa: PLC0414 +from .render import AbstractStyle as AbstractStyle # noqa: PLC0414 +from .render import AsciiStyle as AsciiStyle # noqa: PLC0414 +from .render import ContRoundStyle as ContRoundStyle # noqa: PLC0414 +from .render import ContStyle as ContStyle # noqa: PLC0414 +from .render import DoubleStyle as DoubleStyle # noqa: PLC0414 +from .render import RenderTree as RenderTree # noqa: PLC0414 +from .resolver import ChildResolverError as ChildResolverError # noqa: PLC0414 +from .resolver import Resolver as Resolver # noqa: PLC0414 +from .resolver import ResolverError as ResolverError # noqa: PLC0414 +from .resolver import RootResolverError as RootResolverError # noqa: PLC0414 +from .search import CountError as CountError # noqa: PLC0414 +from .search import find as find # noqa: PLC0414 +from .search import find_by_attr as find_by_attr # noqa: PLC0414 +from .search import findall as findall # noqa: PLC0414 +from .search import findall_by_attr as findall_by_attr # noqa: PLC0414 +from .walker import Walker as Walker # noqa: PLC0414 +from .walker import WalkError as WalkError # noqa: PLC0414 + +# legacy +LevelGroupOrderIter = LevelOrderGroupIter diff --git a/pyproject.toml b/pyproject.toml index 3182c8d..5d8f1e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ [dependency-groups] dev = [ "coveralls>=3.3.1", - "mypy>=1.9.0", + "mypy>=1.17.0", "pytest-cov>=5.0.0", "ruff>=0.11.2", "pre-commit>=4.2.0", @@ -194,5 +194,38 @@ exclude_lines = [ ] [tool.mypy] -disable_error_code = "misc" +files = ["src/anytree"] +exclude = [ + # TODO: Remove these exclusions add missing type annotations + "src/anytree/importer", + "src/anytree/exporter", + "src/anytree/iterators/preorderiter.py", + "src/anytree/iterators/postorderiter.py", + "src/anytree/iterators/levelorderiter.py", + "src/anytree/iterators/zigzaggroupiter.py", + "src/anytree/iterators/levelordergroupiter.py", + "src/anytree/util", + "src/anytree/dotexport.py", + "src/anytree/cachedsearch.py", + "src/anytree/resolver.py", + "src/anytree/search.py", + "src/anytree/render.py", + "src/anytree/walker.py", +] +check_untyped_defs = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_defs = true ignore_missing_imports = true +no_implicit_optional = true +no_implicit_reexport = true +show_column_numbers = true +show_error_codes = true +show_traceback = true +strict = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true diff --git a/src/anytree/iterators/__init__.py b/src/anytree/iterators/__init__.py index 4af768d..a735f1a 100644 --- a/src/anytree/iterators/__init__.py +++ b/src/anytree/iterators/__init__.py @@ -8,12 +8,12 @@ * :any:`ZigZagGroupIter`: iterate over tree using level-order strategy returning group for every level """ -from .abstractiter import AbstractIter -from .levelordergroupiter import LevelOrderGroupIter -from .levelorderiter import LevelOrderIter -from .postorderiter import PostOrderIter -from .preorderiter import PreOrderIter -from .zigzaggroupiter import ZigZagGroupIter +from .abstractiter import AbstractIter as AbstractIter # noqa: PLC0414 +from .levelordergroupiter import LevelOrderGroupIter as LevelOrderGroupIter # noqa: PLC0414 +from .levelorderiter import LevelOrderIter as LevelOrderIter # noqa: PLC0414 +from .postorderiter import PostOrderIter as PostOrderIter # noqa: PLC0414 +from .preorderiter import PreOrderIter as PreOrderIter # noqa: PLC0414 +from .zigzaggroupiter import ZigZagGroupIter as ZigZagGroupIter # noqa: PLC0414 __all__ = [ "AbstractIter", diff --git a/src/anytree/iterators/abstractiter.py b/src/anytree/iterators/abstractiter.py index c82f8a6..ad42c9a 100644 --- a/src/anytree/iterators/abstractiter.py +++ b/src/anytree/iterators/abstractiter.py @@ -1,4 +1,20 @@ -class AbstractIter: +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + + from typing_extensions import Self + + from anytree.node.lightnodemixin import LightNodeMixin + from anytree.node.nodemixin import NodeMixin + + +NodeT_co = TypeVar("NodeT_co", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) + + +class AbstractIter(Generic[NodeT_co]): # pylint: disable=R0205 """ Iterate over tree starting at `node`. @@ -11,14 +27,20 @@ class AbstractIter: maxlevel (int): maximum descending in the node hierarchy. """ - def __init__(self, node, filter_=None, stop=None, maxlevel=None): + def __init__( + self, + node: NodeT_co, + filter_: Callable[[NodeT_co], bool] | None = None, + stop: Callable[[NodeT_co], bool] | None = None, + maxlevel: int | None = None, + ) -> None: self.node = node self.filter_ = filter_ self.stop = stop self.maxlevel = maxlevel - self.__iter = None + self.__iter: Iterator[NodeT_co] | None = None - def __init(self): + def __init(self) -> Iterator[NodeT_co]: node = self.node maxlevel = self.maxlevel filter_ = self.filter_ or AbstractIter.__default_filter @@ -27,31 +49,36 @@ def __init(self): return self._iter(children, filter_, stop, maxlevel) @staticmethod - def __default_filter(node): + def __default_filter(node: NodeT_co) -> bool: # pylint: disable=W0613 return True @staticmethod - def __default_stop(node): + def __default_stop(node: NodeT_co) -> bool: # pylint: disable=W0613 return False - def __iter__(self): + def __iter__(self) -> Self: return self - def __next__(self): + def __next__(self) -> NodeT_co: if self.__iter is None: self.__iter = self.__init() return next(self.__iter) @staticmethod - def _iter(children, filter_, stop, maxlevel): - raise NotImplementedError + def _iter( + children: Iterable[NodeT_co], + filter_: Callable[[NodeT_co], bool], + stop: Callable[[NodeT_co], bool], + maxlevel: int | None, + ) -> Iterator[NodeT_co]: + raise NotImplementedError # pragma: no cover @staticmethod - def _abort_at_level(level, maxlevel): + def _abort_at_level(level: int, maxlevel: int | None) -> bool: return maxlevel is not None and level > maxlevel @staticmethod - def _get_children(children, stop): + def _get_children(children: Iterable[NodeT_co], stop: Callable[[NodeT_co], bool]) -> list[Any]: return [child for child in children if not stop(child)] diff --git a/src/anytree/node/__init__.py b/src/anytree/node/__init__.py index 3336d03..4bca914 100644 --- a/src/anytree/node/__init__.py +++ b/src/anytree/node/__init__.py @@ -9,13 +9,14 @@ * :any:`LightNodeMixin`: A :any:`NodeMixin` using slots. """ -from .anynode import AnyNode -from .exceptions import LoopError, TreeError -from .lightnodemixin import LightNodeMixin -from .node import Node -from .nodemixin import NodeMixin -from .symlinknode import SymlinkNode -from .symlinknodemixin import SymlinkNodeMixin +from .anynode import AnyNode as AnyNode # noqa: PLC0414 +from .exceptions import LoopError as LoopError # noqa: PLC0414 +from .exceptions import TreeError as TreeError # noqa: PLC0414 +from .lightnodemixin import LightNodeMixin as LightNodeMixin # noqa: PLC0414 +from .node import Node as Node # noqa: PLC0414 +from .nodemixin import NodeMixin as NodeMixin # noqa: PLC0414 +from .symlinknode import SymlinkNode as SymlinkNode # noqa: PLC0414 +from .symlinknodemixin import SymlinkNodeMixin as SymlinkNodeMixin # noqa: PLC0414 __all__ = [ "AnyNode", diff --git a/src/anytree/node/anynode.py b/src/anytree/node/anynode.py index 527940b..5d9199f 100644 --- a/src/anytree/node/anynode.py +++ b/src/anytree/node/anynode.py @@ -1,8 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from .nodemixin import NodeMixin from .util import _repr +if TYPE_CHECKING: + from collections.abc import Iterable + -class AnyNode(NodeMixin): +class AnyNode(NodeMixin["AnyNode"]): """ A generic tree node with any `kwargs`. @@ -90,11 +97,11 @@ class AnyNode(NodeMixin): ... ]) """ - def __init__(self, parent=None, children=None, **kwargs): + def __init__(self, parent: AnyNode | None = None, children: Iterable[AnyNode] | None = None, **kwargs: Any) -> None: self.__dict__.update(kwargs) self.parent = parent if children: self.children = children - def __repr__(self): + def __repr__(self) -> str: return _repr(self) diff --git a/src/anytree/node/lightnodemixin.py b/src/anytree/node/lightnodemixin.py index 08d3447..336d92a 100644 --- a/src/anytree/node/lightnodemixin.py +++ b/src/anytree/node/lightnodemixin.py @@ -1,10 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast + from anytree.config import ASSERTIONS from anytree.iterators import PreOrderIter from .exceptions import LoopError, TreeError +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from typing_extensions import Any + + from .nodemixin import NodeMixin + +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) + -class LightNodeMixin: +class LightNodeMixin(Generic[NodeT_co]): """ The :any:`LightNodeMixin` behaves identical to :any:`NodeMixin`, but uses `__slots__`. @@ -83,7 +96,7 @@ class LightNodeMixin: separator = "/" @property - def parent(self): + def parent(self) -> NodeT_co | None: """ Parent Node. @@ -123,7 +136,7 @@ def parent(self): return None @parent.setter - def parent(self, value): + def parent(self, value: NodeT_co | None) -> None: if hasattr(self, "_LightNodeMixin__parent"): parent = self.__parent else: @@ -133,7 +146,7 @@ def parent(self, value): self.__detach(parent) self.__attach(value) - def __check_loop(self, node): + def __check_loop(self, node: NodeT_co | None) -> None: if node is not None: if node is self: msg = "Cannot set parent. %r cannot be parent of itself." @@ -142,7 +155,7 @@ def __check_loop(self, node): msg = "Cannot set parent. %r is parent of %r." raise LoopError(msg % (self, node)) - def __detach(self, parent): + def __detach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -151,11 +164,11 @@ def __detach(self, parent): 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 + self.__parent: NodeT_co | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent): + def __attach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -169,13 +182,57 @@ def __attach(self, parent): self._post_attach(parent) @property - def __children_or_empty(self): + def __children_or_empty(self) -> list[NodeT_co]: if not hasattr(self, "_LightNodeMixin__children"): - self.__children = [] + self.__children: list[NodeT_co] = [] return self.__children - @property - def children(self): + def __children_get(self) -> tuple[NodeT_co, ...]: + return tuple(self.__children_or_empty) + + @staticmethod + def __check_children(children: Iterable[object]) -> None: + seen = set() + for child in children: + childid = id(child) + if childid not in seen: + seen.add(childid) + else: + msg = f"Cannot add node {child!r} multiple times as child." + raise TreeError(msg) + + def __children_set(self, children: tuple[NodeT_co, ...]) -> None: + # convert iterable to tuple + children = tuple(children) + self.__check_children(children) + # ATOMIC start + old_children = self.children + del self.children + try: + self._pre_attach_children(children) + for child in children: + child.parent = self + self._post_attach_children(children) + if ASSERTIONS: # pragma: no branch + assert len(self.children) == len(children) + except Exception: + self.children = old_children + raise + # ATOMIC end + + def __children_del(self) -> None: + children = self.children + self._pre_detach_children(children) + for child in self.children: + child.parent = None + if ASSERTIONS: # pragma: no branch + assert len(self.children) == 0 + self._post_detach_children(children) + + children = property( + __children_get, + __children_set, + __children_del, """ All child nodes. @@ -222,64 +279,23 @@ def children(self): Traceback (most recent call last): ... anytree.node.exceptions.TreeError: Cannot add node Node('/n/a') multiple times as child. - """ - return tuple(self.__children_or_empty) - - @staticmethod - def __check_children(children): - seen = set() - for child in children: - childid = id(child) - if childid not in seen: - seen.add(childid) - else: - msg = f"Cannot add node {child!r} multiple times as child." - raise TreeError(msg) - - @children.setter # type: ignore[no-redef] - def children(self, children): - # convert iterable to tuple - children = tuple(children) - LightNodeMixin.__check_children(children) - # ATOMIC start - old_children = self.children - del self.children - try: - self._pre_attach_children(children) - for child in children: - child.parent = self - self._post_attach_children(children) - if ASSERTIONS: # pragma: no branch - assert len(self.children) == len(children) - except Exception: - self.children = old_children - raise - # ATOMIC end - - @children.deleter # type: ignore[no-redef] - def children(self): - children = self.children - self._pre_detach_children(children) - for child in self.children: - child.parent = None - if ASSERTIONS: # pragma: no branch - assert len(self.children) == 0 - self._post_detach_children(children) + """, + ) - def _pre_detach_children(self, children): + def _pre_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children): + def _post_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children): + def _pre_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children): + def _post_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self): + def path(self) -> tuple[NodeT_co, ...]: """ Path from root node down to this `Node`. @@ -296,7 +312,7 @@ def path(self): """ return self._path - def iter_path_reverse(self): + def iter_path_reverse(self) -> Generator[NodeT_co, None, None]: """ Iterate up the tree from the current node to the root node. @@ -317,17 +333,17 @@ def iter_path_reverse(self): Node('/Udo/Marc') Node('/Udo') """ - node = self + node: NodeT_co | None = cast("NodeT_co", self) while node is not None: yield node node = node.parent @property - def _path(self): + def _path(self) -> tuple[NodeT_co, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self): + def ancestors(self) -> tuple[NodeT_co, ...]: """ All parent nodes and their parent nodes. @@ -347,7 +363,7 @@ def ancestors(self): return self.parent.path @property - def descendants(self): + def descendants(self) -> tuple[NodeT_co, ...]: """ All child nodes and all their child nodes. @@ -367,7 +383,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT_co: """ Tree Root Node. @@ -382,13 +398,13 @@ def root(self): >>> lian.root Node('/Udo') """ - node = self + node: NodeT_co = cast("NodeT_co", self) while node.parent is not None: node = node.parent return node @property - def siblings(self): + def siblings(self) -> tuple[NodeT_co, ...]: """ Tuple of nodes with the same parent. @@ -413,7 +429,7 @@ def siblings(self): return tuple(node for node in parent.children if node is not self) @property - def leaves(self): + def leaves(self) -> tuple[NodeT_co, ...]: """ Tuple of all leaf nodes. @@ -431,7 +447,7 @@ def leaves(self): return tuple(PreOrderIter(self, filter_=lambda node: node.is_leaf)) @property - def is_leaf(self): + def is_leaf(self) -> bool: """ `Node` has no children (External Node). @@ -449,7 +465,7 @@ def is_leaf(self): return len(self.__children_or_empty) == 0 @property - def is_root(self): + def is_root(self) -> bool: """ `Node` is tree root. @@ -467,7 +483,7 @@ def is_root(self): return self.parent is None @property - def height(self): + def height(self) -> int: """ Number of edges on the longest path to a leaf `Node`. @@ -488,7 +504,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -510,7 +526,7 @@ def depth(self): return depth @property - def size(self): + def size(self) -> int: """ Tree size --- the number of nodes in tree starting at this node. @@ -535,14 +551,14 @@ def size(self): continue return size - def _pre_detach(self, parent): + def _pre_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent): + def _post_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent): + def _pre_attach(self, parent: NodeT_co | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent): + def _post_attach(self, parent: NodeT_co | None) -> None: """Method call after attaching to `parent`.""" diff --git a/src/anytree/node/node.py b/src/anytree/node/node.py index 05ae187..94817b3 100644 --- a/src/anytree/node/node.py +++ b/src/anytree/node/node.py @@ -1,8 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from .nodemixin import NodeMixin from .util import _repr +if TYPE_CHECKING: + from collections.abc import Iterable + -class Node(NodeMixin): +class Node(NodeMixin["Node"]): """ A simple tree node with a `name` and any `kwargs`. @@ -69,13 +76,19 @@ class Node(NodeMixin): └── Node('/root/sub1/sub1C/sub1Ca') """ - def __init__(self, name, parent=None, children=None, **kwargs): + def __init__( + self, + name: str, + parent: Node | None = None, + children: Iterable[Node] | None = None, + **kwargs: Any, + ) -> None: self.__dict__.update(kwargs) self.name = name self.parent = parent if children: self.children = children - def __repr__(self): + def __repr__(self) -> str: args = ["{!r}".format(self.separator.join([""] + [str(node.name) for node in self.path]))] return _repr(self, args=args, nameblacklist=["name"]) diff --git a/src/anytree/node/nodemixin.py b/src/anytree/node/nodemixin.py index 542884a..2cde95a 100644 --- a/src/anytree/node/nodemixin.py +++ b/src/anytree/node/nodemixin.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import warnings +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast from anytree.config import ASSERTIONS from anytree.iterators import PreOrderIter @@ -6,8 +9,16 @@ from .exceptions import LoopError, TreeError from .lightnodemixin import LightNodeMixin +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from typing_extensions import Any + + +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) -class NodeMixin: + +class NodeMixin(Generic[NodeT_co]): """ The :any:`NodeMixin` class extends any Python class to a tree node. @@ -77,10 +88,29 @@ class NodeMixin: separator = "/" - @property - def parent(self): - """ - Parent Node. + def __parent_get(self) -> NodeT_co | None: + if hasattr(self, "_NodeMixin__parent"): + return self.__parent + return None + + def __parent_set(self, value: object | None) -> None: + if value is not None and not isinstance(value, (NodeMixin, LightNodeMixin)): + msg = f"Parent node {value!r} is not of type 'NodeMixin'." + raise TreeError(msg) + if hasattr(self, "_NodeMixin__parent"): + parent = self.__parent + else: + parent = None + if parent is not value: + value_co = cast("NodeT_co", value) + self.__check_loop(value_co) + self.__detach(parent) + self.__attach(value_co) + + parent = property( + __parent_get, + __parent_set, + doc="""Parent Node. On set, the node is detached from any previous parent node and attached to the new node. @@ -112,26 +142,10 @@ def parent(self): >>> marc.parent = None >>> marc.is_root True - """ - if hasattr(self, "_NodeMixin__parent"): - return self.__parent - return None - - @parent.setter - def parent(self, value): - if value is not None and not isinstance(value, (NodeMixin, LightNodeMixin)): - msg = f"Parent node {value!r} is not of type 'NodeMixin'." - raise TreeError(msg) - if hasattr(self, "_NodeMixin__parent"): - parent = self.__parent - else: - parent = None - if parent is not value: - self.__check_loop(value) - self.__detach(parent) - self.__attach(value) + """, + ) - def __check_loop(self, node): + def __check_loop(self, node: NodeT_co | None) -> None: if node is not None: if node is self: msg = "Cannot set parent. %r cannot be parent of itself." @@ -140,7 +154,7 @@ def __check_loop(self, node): msg = "Cannot set parent. %r is parent of %r." raise LoopError(msg % (self, node)) - def __detach(self, parent): + def __detach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -149,11 +163,11 @@ def __detach(self, parent): 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 + self.__parent: NodeT_co | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent): + def __attach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -167,13 +181,60 @@ def __attach(self, parent): self._post_attach(parent) @property - def __children_or_empty(self): + def __children_or_empty(self) -> list[NodeT_co]: if not hasattr(self, "_NodeMixin__children"): - self.__children = [] + self.__children: list[NodeT_co] = [] return self.__children - @property - def children(self): + def __children_get(self) -> tuple[NodeT_co, ...]: + return tuple(self.__children_or_empty) + + @staticmethod + def __check_children(children: Iterable[object]) -> None: + seen = set() + for child in children: + if not isinstance(child, (NodeMixin, LightNodeMixin)): + msg = f"Cannot add non-node object {child!r}. It is not a subclass of 'NodeMixin' or 'LightNodeMixin'." + raise TreeError(msg) + childid = id(child) + if childid not in seen: + seen.add(childid) + else: + msg = f"Cannot add node {child!r} multiple times as child." + raise TreeError(msg) + + def __children_set(self, children: Iterable[NodeT_co]) -> None: + # convert iterable to tuple + children = tuple(children) + NodeMixin.__check_children(children) + # ATOMIC start + old_children = self.children + del self.children + try: + self._pre_attach_children(children) + for child in children: + child.parent = self + self._post_attach_children(children) + if ASSERTIONS: # pragma: no branch + assert len(self.children) == len(children) + except Exception: + self.children = old_children + raise + # ATOMIC end + + def __children_del(self) -> None: + children = self.children + self._pre_detach_children(children) + for child in self.children: + child.parent = None + if ASSERTIONS: # pragma: no branch + assert len(self.children) == 0 + self._post_detach_children(children) + + children = property( + __children_get, + __children_set, + __children_del, """ All child nodes. @@ -220,67 +281,23 @@ def children(self): Traceback (most recent call last): ... anytree.node.exceptions.TreeError: Cannot add node Node('/n/a') multiple times as child. - """ - return tuple(self.__children_or_empty) - - @staticmethod - def __check_children(children): - seen = set() - for child in children: - if not isinstance(child, (NodeMixin, LightNodeMixin)): - msg = f"Cannot add non-node object {child!r}. It is not a subclass of 'NodeMixin'." - raise TreeError(msg) - childid = id(child) - if childid not in seen: - seen.add(childid) - else: - msg = f"Cannot add node {child!r} multiple times as child." - raise TreeError(msg) - - @children.setter # type: ignore[no-redef] - def children(self, children): - # convert iterable to tuple - children = tuple(children) - NodeMixin.__check_children(children) - # ATOMIC start - old_children = self.children - del self.children - try: - self._pre_attach_children(children) - for child in children: - child.parent = self - self._post_attach_children(children) - if ASSERTIONS: # pragma: no branch - assert len(self.children) == len(children) - except Exception: - self.children = old_children - raise - # ATOMIC end - - @children.deleter # type: ignore[no-redef] - def children(self): - children = self.children - self._pre_detach_children(children) - for child in self.children: - child.parent = None - if ASSERTIONS: # pragma: no branch - assert len(self.children) == 0 - self._post_detach_children(children) + """, + ) - def _pre_detach_children(self, children): + def _pre_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children): + def _post_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children): + def _pre_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children): + def _post_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self): + def path(self) -> tuple[NodeT_co, ...]: """ Path from root node down to this `Node`. @@ -297,7 +314,7 @@ def path(self): """ return self._path - def iter_path_reverse(self): + def iter_path_reverse(self) -> Generator[NodeT_co, None, None]: """ Iterate up the tree from the current node to the root node. @@ -318,17 +335,17 @@ def iter_path_reverse(self): Node('/Udo/Marc') Node('/Udo') """ - node = self + node: NodeT_co | None = cast("NodeT_co", self) while node is not None: yield node node = node.parent @property - def _path(self): + def _path(self) -> tuple[NodeT_co, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self): + def ancestors(self) -> tuple[NodeT_co, ...]: """ All parent nodes and their parent nodes. @@ -348,18 +365,18 @@ def ancestors(self): return self.parent.path @property - def anchestors(self): + def anchestors(self) -> tuple[NodeT_co, ...]: # codespell:ignore anchestors """ All parent nodes and their parent nodes - see :any:`ancestors`. - The attribute `anchestors` is just a typo of `ancestors`. Please use `ancestors`. + This attribute is just a typo of `ancestors`. Please use `ancestors`. This attribute will be removed in the 3.0.0 release. """ warnings.warn(".anchestors was a typo and will be removed in version 3.0.0", DeprecationWarning, stacklevel=2) return self.ancestors @property - def descendants(self): + def descendants(self) -> tuple[NodeT_co, ...]: """ All child nodes and all their child nodes. @@ -379,7 +396,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT_co: """ Tree Root Node. @@ -394,13 +411,13 @@ def root(self): >>> lian.root Node('/Udo') """ - node = self + node: NodeT_co = cast("NodeT_co", self) while node.parent is not None: node = node.parent return node @property - def siblings(self): + def siblings(self) -> tuple[NodeT_co, ...]: """ Tuple of nodes with the same parent. @@ -425,7 +442,7 @@ def siblings(self): return tuple(node for node in parent.children if node is not self) @property - def leaves(self): + def leaves(self) -> tuple[NodeT_co, ...]: """ Tuple of all leaf nodes. @@ -443,7 +460,7 @@ def leaves(self): return tuple(PreOrderIter(self, filter_=lambda node: node.is_leaf)) @property - def is_leaf(self): + def is_leaf(self) -> bool: """ `Node` has no children (External Node). @@ -461,7 +478,7 @@ def is_leaf(self): return len(self.__children_or_empty) == 0 @property - def is_root(self): + def is_root(self) -> bool: """ `Node` is tree root. @@ -479,7 +496,7 @@ def is_root(self): return self.parent is None @property - def height(self): + def height(self) -> int: """ Number of edges on the longest path to a leaf `Node`. @@ -500,7 +517,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -522,7 +539,7 @@ def depth(self): return depth @property - def size(self): + def size(self) -> int: """ Tree size --- the number of nodes in tree starting at this node. @@ -547,14 +564,14 @@ def size(self): continue return size - def _pre_detach(self, parent): + def _pre_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent): + def _post_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent): + def _pre_attach(self, parent: NodeT_co | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent): + def _post_attach(self, parent: NodeT_co | None) -> None: """Method call after attaching to `parent`.""" diff --git a/src/anytree/node/symlinknode.py b/src/anytree/node/symlinknode.py index 7e2c44d..5faf435 100644 --- a/src/anytree/node/symlinknode.py +++ b/src/anytree/node/symlinknode.py @@ -1,8 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar + from .symlinknodemixin import SymlinkNodeMixin from .util import _repr +if TYPE_CHECKING: + from collections.abc import Iterable + + from .lightnodemixin import LightNodeMixin + from .nodemixin import NodeMixin + + +NodeT_co = TypeVar("NodeT_co", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) + -class SymlinkNode(SymlinkNodeMixin): +class SymlinkNode(SymlinkNodeMixin, Generic[NodeT_co]): """ Tree node which references to another tree node. @@ -42,12 +55,18 @@ class SymlinkNode(SymlinkNodeMixin): 9 """ - def __init__(self, target, parent=None, children=None, **kwargs): + def __init__( + self, + target: NodeT_co, + parent: SymlinkNode[NodeT_co] | None = None, + children: Iterable[SymlinkNode[NodeT_co]] | None = None, + **kwargs: Any, + ) -> None: self.target = target self.target.__dict__.update(kwargs) self.parent = parent if children: self.children = children - def __repr__(self): + def __repr__(self) -> str: return _repr(self, [repr(self.target)], nameblacklist=("target",)) diff --git a/src/anytree/node/symlinknodemixin.py b/src/anytree/node/symlinknodemixin.py index 3500a59..5892f5a 100644 --- a/src/anytree/node/symlinknodemixin.py +++ b/src/anytree/node/symlinknodemixin.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from .nodemixin import NodeMixin -class SymlinkNodeMixin(NodeMixin): +class SymlinkNodeMixin(NodeMixin["SymlinkNodeMixin"]): """ The :any:`SymlinkNodeMixin` class extends any Python class to a symbolic link to a tree node. @@ -44,14 +46,14 @@ class SymlinkNodeMixin(NodeMixin): 9 """ - def __getattr__(self, name): + def __getattr__(self, name: str) -> object: if name in ("_NodeMixin__parent", "_NodeMixin__children"): - return super().__getattr__(name) + return super().__getattr__(name) # type: ignore[misc] if name == "__setstate__": raise AttributeError(name) return getattr(self.target, name) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: object) -> None: if name in ("_NodeMixin__parent", "_NodeMixin__children", "parent", "children", "target"): super().__setattr__(name, value) else: diff --git a/src/anytree/node/util.py b/src/anytree/node/util.py index 860e27a..75a2d88 100644 --- a/src/anytree/node/util.py +++ b/src/anytree/node/util.py @@ -1,4 +1,14 @@ -def _repr(node, args=None, nameblacklist=None): +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Sequence + + from .nodemixin import NodeMixin + + +def _repr(node: NodeMixin[Any], args: list[str] | None = None, nameblacklist: Sequence[str] | None = None) -> str: classname = node.__class__.__name__ args = args or [] nameblacklist = nameblacklist or [] diff --git a/src/anytree/py.typed b/src/anytree/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_node.py b/tests/test_node.py index ffb61d9..04a707a 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -5,7 +5,7 @@ def test_node_parent_error(): """Node Parent Error.""" - with assert_raises(TreeError, "Parent node 'parent' is not of type 'NodeMixin'."): + with assert_raises(TreeError, "Parent node 'parent' is not of type 'NodeMixin' or 'LightNodeMixin'."): Node("root", "parent") @@ -177,7 +177,9 @@ def test_children_setter_large(): def test_node_children_type(): root = Node("root") - with assert_raises(TreeError, "Cannot add non-node object 'string'. It is not a subclass of 'NodeMixin'."): + with assert_raises( + TreeError, "Cannot add non-node object 'string'. It is not a subclass of 'NodeMixin' or 'LightNodeMixin'." + ): root.children = ["string"] @@ -558,6 +560,9 @@ def __eq__(self, other): return self.a == other.a and self.b == other.b return NotImplemented + def __hash__(self): + return NotImplemented + r = EqOverwrittingNode(0, 0) a = EqOverwrittingNode(1, 0, parent=r) b = EqOverwrittingNode(1, 0, parent=r) @@ -571,12 +576,14 @@ def __eq__(self, other): def test_tuple(): """Tuple as parent.""" - with assert_raises(TreeError, "Parent node (1, 0, 3) is not of type 'NodeMixin'."): + with assert_raises(TreeError, "Parent node (1, 0, 3) is not of type 'NodeMixin' or 'LightNodeMixin'."): Node((0, 1, 2), parent=(1, 0, 3)) def test_tuple_as_children(): """Tuple as children.""" n = Node("foo") - with assert_raises(TreeError, "Cannot add non-node object (0, 1, 2). It is not a subclass of 'NodeMixin'."): + with assert_raises( + TreeError, "Cannot add non-node object (0, 1, 2). It is not a subclass of 'NodeMixin' or 'LightNodeMixin'." + ): n.children = [(0, 1, 2)]