diff --git a/anytree/__init__.py b/anytree/__init__.py index 5088206..c84bafd 100644 --- a/anytree/__init__.py +++ b/anytree/__init__.py @@ -8,38 +8,39 @@ __description__ = """Powerful and Lightweight Python Tree Data Structure.""" __url__ = "https://github.com/c0fec0de/anytree" -from . import cachedsearch # noqa -from . import util # noqa -from .iterators import LevelOrderGroupIter # noqa -from .iterators import LevelOrderIter # noqa -from .iterators import PostOrderIter # noqa -from .iterators import PreOrderIter # noqa -from .iterators import ZigZagGroupIter # noqa -from .node import AnyNode # noqa -from .node import LightNodeMixin # noqa -from .node import LoopError # noqa -from .node import Node # noqa -from .node import NodeMixin # noqa -from .node import SymlinkNode # noqa -from .node import SymlinkNodeMixin # noqa -from .node import TreeError # noqa -from .render import AbstractStyle # noqa -from .render import AsciiStyle # noqa -from .render import ContRoundStyle # noqa -from .render import ContStyle # noqa -from .render import DoubleStyle # noqa -from .render import RenderTree # noqa -from .resolver import ChildResolverError # noqa -from .resolver import Resolver # noqa -from .resolver import ResolverError # noqa -from .resolver import RootResolverError # noqa -from .search import CountError # noqa -from .search import find # noqa -from .search import find_by_attr # noqa -from .search import findall # noqa -from .search import findall_by_attr # noqa -from .walker import Walker # noqa -from .walker import WalkError # noqa +# pylint: disable=useless-import-alias +from . import cachedsearch as cachedsearch # noqa +from . import util as util # noqa +from .iterators import LevelOrderGroupIter as LevelOrderGroupIter # noqa +from .iterators import LevelOrderIter as LevelOrderIter # noqa +from .iterators import PostOrderIter as PostOrderIter # noqa +from .iterators import PreOrderIter as PreOrderIter # noqa +from .iterators import ZigZagGroupIter as ZigZagGroupIter # noqa +from .node import AnyNode as AnyNode # noqa +from .node import LightNodeMixin as LightNodeMixin # noqa +from .node import LoopError as LoopError # noqa +from .node import Node as Node # noqa +from .node import NodeMixin as NodeMixin # noqa +from .node import SymlinkNode as SymlinkNode # noqa +from .node import SymlinkNodeMixin as SymlinkNodeMixin # noqa +from .node import TreeError as TreeError # noqa +from .render import AbstractStyle as AbstractStyle # noqa +from .render import AsciiStyle as AsciiStyle # noqa +from .render import ContRoundStyle as ContRoundStyle # noqa +from .render import ContStyle as ContStyle # noqa +from .render import DoubleStyle as DoubleStyle # noqa +from .render import RenderTree as RenderTree # noqa +from .resolver import ChildResolverError as ChildResolverError # noqa +from .resolver import Resolver as Resolver # noqa +from .resolver import ResolverError as ResolverError # noqa +from .resolver import RootResolverError as RootResolverError # noqa +from .search import CountError as CountError # noqa +from .search import find as find # noqa +from .search import find_by_attr as find_by_attr # noqa +from .search import findall as findall # noqa +from .search import findall_by_attr as findall_by_attr # noqa +from .walker import Walker as Walker # noqa +from .walker import WalkError as WalkError # noqa # legacy LevelGroupOrderIter = LevelOrderGroupIter diff --git a/anytree/exporter/mermaidexporter.py b/anytree/exporter/mermaidexporter.py index bfb725a..ee6ccf8 100644 --- a/anytree/exporter/mermaidexporter.py +++ b/anytree/exporter/mermaidexporter.py @@ -10,7 +10,6 @@ class MermaidExporter: - """ Mermaid Exporter. diff --git a/anytree/iterators/__init__.py b/anytree/iterators/__init__.py index 4a8e231..961104b 100644 --- a/anytree/iterators/__init__.py +++ b/anytree/iterators/__init__.py @@ -9,9 +9,10 @@ * :any:`ZigZagGroupIter`: iterate over tree using level-order strategy returning group for every level """ -from .abstractiter import AbstractIter # noqa -from .levelordergroupiter import LevelOrderGroupIter # noqa -from .levelorderiter import LevelOrderIter # noqa -from .postorderiter import PostOrderIter # noqa -from .preorderiter import PreOrderIter # noqa -from .zigzaggroupiter import ZigZagGroupIter # noqa +# pylint: disable=useless-import-alias +from .abstractiter import AbstractIter as AbstractIter # noqa +from .levelordergroupiter import LevelOrderGroupIter as LevelOrderGroupIter # noqa +from .levelorderiter import LevelOrderIter as LevelOrderIter # noqa +from .postorderiter import PostOrderIter as PostOrderIter # noqa +from .preorderiter import PreOrderIter as PreOrderIter # noqa +from .zigzaggroupiter import ZigZagGroupIter as ZigZagGroupIter # noqa diff --git a/anytree/iterators/abstractiter.py b/anytree/iterators/abstractiter.py index 055cecd..960ae4c 100644 --- a/anytree/iterators/abstractiter.py +++ b/anytree/iterators/abstractiter.py @@ -1,7 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar + import six +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Iterator + + from typing_extensions import Self + + from ..node.lightnodemixin import LightNodeMixin + from ..node.nodemixin import NodeMixin + + +NodeT_co = TypeVar("NodeT_co", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) + -class AbstractIter(six.Iterator): +class AbstractIter(Generic[NodeT_co], six.Iterator): # pylint: disable=R0205 """ Iterate over tree starting at `node`. @@ -14,14 +29,20 @@ class AbstractIter(six.Iterator): 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 @@ -30,31 +51,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): + 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/anytree/node/__init__.py b/anytree/node/__init__.py index 71221d7..71b6319 100644 --- a/anytree/node/__init__.py +++ b/anytree/node/__init__.py @@ -9,11 +9,12 @@ * :any:`LightNodeMixin`: A :any:`NodeMixin` using slots. """ -from .anynode import AnyNode # noqa -from .exceptions import LoopError # noqa -from .exceptions import TreeError # noqa -from .lightnodemixin import LightNodeMixin # noqa -from .node import Node # noqa -from .nodemixin import NodeMixin # noqa -from .symlinknode import SymlinkNode # noqa -from .symlinknodemixin import SymlinkNodeMixin # noqa +# pylint: disable=useless-import-alias +from .anynode import AnyNode as AnyNode # noqa +from .exceptions import LoopError as LoopError # noqa +from .exceptions import TreeError as TreeError # noqa +from .lightnodemixin import LightNodeMixin as LightNodeMixin # noqa +from .node import Node as Node # noqa +from .nodemixin import NodeMixin as NodeMixin # noqa +from .symlinknode import SymlinkNode as SymlinkNode # noqa +from .symlinknodemixin import SymlinkNodeMixin as SymlinkNodeMixin # noqa diff --git a/anytree/node/anynode.py b/anytree/node/anynode.py index 272061b..dfe2880 100644 --- a/anytree/node/anynode.py +++ b/anytree/node/anynode.py @@ -1,10 +1,17 @@ # -*- coding: utf-8 -*- +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`. @@ -92,12 +99,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/anytree/node/lightnodemixin.py b/anytree/node/lightnodemixin.py index 248e294..70a9acd 100644 --- a/anytree/node/lightnodemixin.py +++ b/anytree/node/lightnodemixin.py @@ -1,13 +1,25 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast + from anytree.iterators import PreOrderIter from ..config import ASSERTIONS from .exceptions import LoopError, TreeError +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from typing_extensions import Any -class LightNodeMixin: + from .nodemixin import NodeMixin +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) + + +class LightNodeMixin(Generic[NodeT_co]): """ The :any:`LightNodeMixin` behaves identical to :any:`NodeMixin`, but uses `__slots__`. @@ -86,7 +98,7 @@ class LightNodeMixin: separator = "/" @property - def parent(self): + def parent(self) -> NodeT_co | None: """ Parent Node. @@ -126,7 +138,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: @@ -136,7 +148,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." @@ -145,7 +157,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) @@ -154,11 +166,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) @@ -172,13 +184,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[NodeT_co]) -> None: + seen = set() + for child in children: + childid = id(child) + if childid not in seen: + seen.add(childid) + else: + msg = "Cannot add node %r multiple times as child." % (child,) + raise TreeError(msg) + + def __children_set(self, children: Iterable[NodeT_co]) -> None: + # 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 + + 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. @@ -225,64 +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: - childid = id(child) - if childid not in seen: - seen.add(childid) - else: - msg = "Cannot add node %r multiple times as child." % (child,) - raise TreeError(msg) - - @children.setter - 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 - 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`. @@ -299,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. @@ -320,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. @@ -350,7 +365,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. @@ -370,7 +385,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT_co: """ Tree Root Node. @@ -385,13 +400,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. @@ -416,7 +431,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. @@ -434,7 +449,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). @@ -452,7 +467,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. @@ -470,7 +485,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`. @@ -491,7 +506,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -513,7 +528,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. @@ -538,14 +553,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/anytree/node/node.py b/anytree/node/node.py index 2ed294a..c52eac8 100644 --- a/anytree/node/node.py +++ b/anytree/node/node.py @@ -1,10 +1,17 @@ # -*- coding: utf-8 -*- +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`. @@ -71,13 +78,15 @@ 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" % self.separator.join([""] + [str(node.name) for node in self.path])] return _repr(self, args=args, nameblacklist=["name"]) diff --git a/anytree/node/nodemixin.py b/anytree/node/nodemixin.py index 46219cf..6222cc2 100644 --- a/anytree/node/nodemixin.py +++ b/anytree/node/nodemixin.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import warnings +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast from anytree.iterators import PreOrderIter @@ -8,9 +11,16 @@ from .exceptions import LoopError, TreeError from .lightnodemixin import LightNodeMixin +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from typing_extensions import Any, TypeGuard + -class NodeMixin: +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) + +class NodeMixin(Generic[NodeT_co]): """ The :any:`NodeMixin` class extends any Python class to a tree node. @@ -81,7 +91,7 @@ class NodeMixin: separator = "/" @property - def parent(self): + def parent(self) -> NodeT_co | None: """ Parent Node. @@ -121,9 +131,12 @@ def parent(self): return None @parent.setter - def parent(self, value): - if value is not None and not isinstance(value, (NodeMixin, LightNodeMixin)): - msg = "Parent node %r is not of type 'NodeMixin'." % (value,) + def parent(self, value: object | None) -> None: + def guard(value: object | None) -> TypeGuard[NodeT_co | None]: + return value is None or isinstance(value, (NodeMixin, LightNodeMixin)) + + if not guard(value): + msg = "Parent node %r is not of type 'NodeMixin' or 'LightNodeMixin'." % (value,) raise TreeError(msg) if hasattr(self, "_NodeMixin__parent"): parent = self.__parent @@ -134,7 +147,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." @@ -143,7 +156,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) @@ -152,11 +165,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) @@ -170,13 +183,62 @@ 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 = "Cannot add non-node object %r. It is not a subclass of 'NodeMixin' or 'LightNodeMixin'." % ( + child, + ) + raise TreeError(msg) + childid = id(child) + if childid not in seen: + seen.add(childid) + else: + msg = "Cannot add node %r multiple times as child." % (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. @@ -223,67 +285,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 = "Cannot add non-node object %r. It is not a subclass of 'NodeMixin'." % (child,) - raise TreeError(msg) - childid = id(child) - if childid not in seen: - seen.add(childid) - else: - msg = "Cannot add node %r multiple times as child." % (child,) - raise TreeError(msg) - - @children.setter - 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 - 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`. @@ -300,7 +318,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. @@ -321,17 +339,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. @@ -351,18 +369,21 @@ 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) + warnings.warn( + ".anchestors was a typo and will be removed in version 3.0.0", # codespell:ignore anchestors + DeprecationWarning, + ) return self.ancestors @property - def descendants(self): + def descendants(self) -> tuple[NodeT_co, ...]: """ All child nodes and all their child nodes. @@ -382,7 +403,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT_co: """ Tree Root Node. @@ -397,13 +418,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. @@ -428,7 +449,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. @@ -446,7 +467,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). @@ -464,7 +485,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. @@ -482,7 +503,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`. @@ -503,7 +524,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -525,7 +546,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. @@ -550,14 +571,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/anytree/node/symlinknode.py b/anytree/node/symlinknode.py index b90706d..3987cef 100644 --- a/anytree/node/symlinknode.py +++ b/anytree/node/symlinknode.py @@ -1,14 +1,28 @@ # -*- coding: utf-8 -*- + +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. Args: - target: Symbolic Link Target. Another tree node, which is refered to. + target: Symbolic Link Target. Another tree node, which is referred to. Keyword Args: parent: Reference to parent node. @@ -43,12 +57,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/anytree/node/symlinknodemixin.py b/anytree/node/symlinknodemixin.py index ee3c727..4d781df 100644 --- a/anytree/node/symlinknodemixin.py +++ b/anytree/node/symlinknodemixin.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- + +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. - The class **MUST** have a `target` attribute refering to another tree node. + The class **MUST** have a `target` attribute referring to another tree node. The :any:`SymlinkNodeMixin` class has its own parent and its own child nodes. All other attribute accesses are just forwarded to the target node. - A minimal implementation looks like (see :any:`SymlinkNode` for a full implemenation): + A minimal implementation looks like (see :any:`SymlinkNode` for a full implementation): >>> from anytree import SymlinkNodeMixin, Node, RenderTree >>> class SymlinkNode(SymlinkNodeMixin): @@ -45,14 +48,14 @@ class SymlinkNodeMixin(NodeMixin): 9 """ - def __getattr__(self, name): + def __getattr__(self, name: str) -> object: if name in ("_NodeMixin__parent", "_NodeMixin__children"): - return super(SymlinkNodeMixin, self).__getattr__(name) + return super(SymlinkNodeMixin, self).__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(SymlinkNodeMixin, self).__setattr__(name, value) else: diff --git a/anytree/node/util.py b/anytree/node/util.py index 6fb71ec..0db221c 100644 --- a/anytree/node/util.py +++ b/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/anytree/py.typed b/anytree/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index d329ac9..bcd100c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,26 @@ exclude = ''' profile = "black" line_length = 120 +[tool.mypy] +mypy_path = "anytree" +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 + [tool.coverage.report] exclude_lines = [ 'return NotImplemented', @@ -117,4 +137,4 @@ commands = poetry run coverage xml poetry run pylint anytree poetry run make html -C docs -""" \ No newline at end of file +""" diff --git a/tests/test_node.py b/tests/test_node.py index 2e7295d..18a2883 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -6,7 +6,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") @@ -182,7 +182,7 @@ 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"] @@ -257,7 +257,7 @@ def test_ancestors(): assert s0a.ancestors == tuple([root, s0]) assert s1ca.ancestors == tuple([root, s1, s1c]) # deprecated typo - assert s1ca.anchestors == tuple([root, s1, s1c]) + assert s1ca.anchestors == tuple([root, s1, s1c]) # codespell:ignore anchestors def test_node_children_init(): @@ -549,7 +549,7 @@ def _post_detach(self, parent): def test_any_node_parent_error(): """Any Node Parent Error.""" - with assert_raises(TreeError, "Parent node 'r' is not of type 'NodeMixin'."): + with assert_raises(TreeError, "Parent node 'r' is not of type 'NodeMixin' or 'LightNodeMixin'."): AnyNode("r") @@ -598,12 +598,12 @@ 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)]