From 4010a228ceb49fb556cff67db808eba403d34a7d Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:03:32 -0600 Subject: [PATCH 1/9] Publically export objects --- anytree/iterators/__init__.py | 12 ++++++------ anytree/node/__init__.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/anytree/iterators/__init__.py b/anytree/iterators/__init__.py index 4a8e231..dc336bf 100644 --- a/anytree/iterators/__init__.py +++ b/anytree/iterators/__init__.py @@ -9,9 +9,9 @@ * :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 +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/node/__init__.py b/anytree/node/__init__.py index 71221d7..978b324 100644 --- a/anytree/node/__init__.py +++ b/anytree/node/__init__.py @@ -9,11 +9,11 @@ * :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 +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 From ae91e32b77d7200589295199bc385706d4ab888f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:31:02 -0600 Subject: [PATCH 2/9] Add type annotations for nodes and abstract iterator --- anytree/iterators/abstractiter.py | 45 ++++++-- anytree/node/anynode.py | 14 ++- anytree/node/lightnodemixin.py | 168 ++++++++++++++------------- anytree/node/node.py | 15 ++- anytree/node/nodemixin.py | 183 ++++++++++++++++-------------- anytree/node/symlinknode.py | 26 ++++- anytree/node/symlinknodemixin.py | 11 +- anytree/node/util.py | 12 +- 8 files changed, 288 insertions(+), 186 deletions(-) diff --git a/anytree/iterators/abstractiter.py b/anytree/iterators/abstractiter.py index 055cecd..fd6bfc0 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 = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) + -class AbstractIter(six.Iterator): +class AbstractIter(six.Iterator, Generic[NodeT]): # 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, + filter_: Callable[[NodeT], bool] | None = None, + stop: Callable[[NodeT], 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] | None = None - def __init(self): + def __init(self) -> Iterator[NodeT]: node = self.node maxlevel = self.maxlevel filter_ = self.filter_ or AbstractIter.__default_filter @@ -30,31 +51,33 @@ def __init(self): return self._iter(children, filter_, stop, maxlevel) @staticmethod - def __default_filter(node): + def __default_filter(node: NodeT) -> bool: # pylint: disable=W0613 return True @staticmethod - def __default_stop(node): + def __default_stop(node: NodeT) -> bool: # pylint: disable=W0613 return False - def __iter__(self): + def __iter__(self) -> Self: return self - def __next__(self): + def __next__(self) -> NodeT: 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], filter_: Callable[[NodeT], bool], stop: Callable[[NodeT], bool], maxlevel: int | None + ) -> Iterator[NodeT]: 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], stop: Callable[[NodeT], bool]) -> list[Any]: return [child for child in children if not stop(child)] diff --git a/anytree/node/anynode.py b/anytree/node/anynode.py index 272061b..0e5ad00 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..0fbc26c 100644 --- a/anytree/node/lightnodemixin.py +++ b/anytree/node/lightnodemixin.py @@ -1,12 +1,23 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic, TypeVar, 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 .nodemixin import NodeMixin -class LightNodeMixin: +NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) + + +class LightNodeMixin(Generic[NodeT]): """ The :any:`LightNodeMixin` behaves identical to :any:`NodeMixin`, but uses `__slots__`. @@ -86,7 +97,7 @@ class LightNodeMixin: separator = "/" @property - def parent(self): + def parent(self) -> NodeT | None: """ Parent Node. @@ -126,7 +137,7 @@ def parent(self): return None @parent.setter - def parent(self, value): + def parent(self, value: NodeT | None) -> None: if hasattr(self, "_LightNodeMixin__parent"): parent = self.__parent else: @@ -136,7 +147,7 @@ def parent(self, value): self.__detach(parent) self.__attach(value) - def __check_loop(self, node): + def __check_loop(self, node: NodeT | None) -> None: if node is not None: if node is self: msg = "Cannot set parent. %r cannot be parent of itself." @@ -145,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 | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -154,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 | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent): + def __attach(self, parent: NodeT | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -172,13 +183,57 @@ def __attach(self, parent): self._post_attach(parent) @property - def __children_or_empty(self): + def __children_or_empty(self) -> list[NodeT]: if not hasattr(self, "_LightNodeMixin__children"): - self.__children = [] + self.__children: list[NodeT] = [] return self.__children - @property - def children(self): + def __children_get(self) -> tuple[NodeT, ...]: + return tuple(self.__children_or_empty) + + @staticmethod + def __check_children(children: Iterable[NodeT]) -> 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]) -> 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 +280,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, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children): + def _post_detach_children(self, children: tuple[NodeT, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children): + def _pre_attach_children(self, children: tuple[NodeT, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children): + def _post_attach_children(self, children: tuple[NodeT, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self): + def path(self) -> tuple[NodeT, ...]: """ Path from root node down to this `Node`. @@ -299,7 +313,7 @@ def path(self): """ return self._path - def iter_path_reverse(self): + def iter_path_reverse(self) -> Generator[NodeT, None, None]: """ Iterate up the tree from the current node to the root node. @@ -320,17 +334,17 @@ def iter_path_reverse(self): Node('/Udo/Marc') Node('/Udo') """ - node = self + node: NodeT | None = cast(NodeT, self) while node is not None: yield node node = node.parent @property - def _path(self): + def _path(self) -> tuple[NodeT, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self): + def ancestors(self) -> tuple[NodeT, ...]: """ All parent nodes and their parent nodes. @@ -350,7 +364,7 @@ def ancestors(self): return self.parent.path @property - def descendants(self): + def descendants(self) -> tuple[NodeT, ...]: """ All child nodes and all their child nodes. @@ -370,7 +384,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT: """ Tree Root Node. @@ -385,13 +399,13 @@ def root(self): >>> lian.root Node('/Udo') """ - node = self + node: NodeT = cast(NodeT, self) while node.parent is not None: node = node.parent return node @property - def siblings(self): + def siblings(self) -> tuple[NodeT, ...]: """ Tuple of nodes with the same parent. @@ -416,7 +430,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, ...]: """ Tuple of all leaf nodes. @@ -434,7 +448,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 +466,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 +484,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 +505,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -513,7 +527,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 +552,14 @@ def size(self): continue return size - def _pre_detach(self, parent): + def _pre_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent): + def _post_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent): + def _pre_attach(self, parent: NodeT | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent): + def _post_attach(self, parent: NodeT | None) -> None: """Method call after attaching to `parent`.""" diff --git a/anytree/node/node.py b/anytree/node/node.py index 2ed294a..efdebeb 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..2e6ae5f 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, Any, Generic, TypeGuard, TypeVar, cast from anytree.iterators import PreOrderIter @@ -8,8 +11,14 @@ from .exceptions import LoopError, TreeError from .lightnodemixin import LightNodeMixin +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + +NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) -class NodeMixin: + +class NodeMixin(Generic[NodeT]): """ The :any:`NodeMixin` class extends any Python class to a tree node. @@ -81,7 +90,7 @@ class NodeMixin: separator = "/" @property - def parent(self): + def parent(self) -> NodeT | None: """ Parent Node. @@ -121,9 +130,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 | 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 +146,7 @@ def parent(self, value): self.__detach(parent) self.__attach(value) - def __check_loop(self, node): + def __check_loop(self, node: NodeT | None) -> None: if node is not None: if node is self: msg = "Cannot set parent. %r cannot be parent of itself." @@ -143,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 | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -152,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 | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent): + def __attach(self, parent: NodeT | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -170,13 +182,62 @@ def __attach(self, parent): self._post_attach(parent) @property - def __children_or_empty(self): + def __children_or_empty(self) -> list[NodeT]: if not hasattr(self, "_NodeMixin__children"): - self.__children = [] + self.__children: list[NodeT] = [] return self.__children - @property - def children(self): + def __children_get(self) -> tuple[NodeT, ...]: + 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]) -> 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 +284,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, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children): + def _post_detach_children(self, children: tuple[NodeT, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children): + def _pre_attach_children(self, children: tuple[NodeT, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children): + def _post_attach_children(self, children: tuple[NodeT, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self): + def path(self) -> tuple[NodeT, ...]: """ Path from root node down to this `Node`. @@ -300,7 +317,7 @@ def path(self): """ return self._path - def iter_path_reverse(self): + def iter_path_reverse(self) -> Generator[NodeT, None, None]: """ Iterate up the tree from the current node to the root node. @@ -321,17 +338,17 @@ def iter_path_reverse(self): Node('/Udo/Marc') Node('/Udo') """ - node = self + node: NodeT | None = cast(NodeT, self) while node is not None: yield node node = node.parent @property - def _path(self): + def _path(self) -> tuple[NodeT, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self): + def ancestors(self) -> tuple[NodeT, ...]: """ All parent nodes and their parent nodes. @@ -351,7 +368,7 @@ def ancestors(self): return self.parent.path @property - def anchestors(self): + def anchestors(self) -> tuple[NodeT, ...]: """ All parent nodes and their parent nodes - see :any:`ancestors`. @@ -362,7 +379,7 @@ def anchestors(self): return self.ancestors @property - def descendants(self): + def descendants(self) -> tuple[NodeT, ...]: """ All child nodes and all their child nodes. @@ -382,7 +399,7 @@ def descendants(self): return tuple(PreOrderIter(self))[1:] @property - def root(self): + def root(self) -> NodeT: """ Tree Root Node. @@ -397,13 +414,13 @@ def root(self): >>> lian.root Node('/Udo') """ - node = self + node: NodeT = cast(NodeT, self) while node.parent is not None: node = node.parent return node @property - def siblings(self): + def siblings(self) -> tuple[NodeT, ...]: """ Tuple of nodes with the same parent. @@ -428,7 +445,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, ...]: """ Tuple of all leaf nodes. @@ -446,7 +463,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 +481,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 +499,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 +520,7 @@ def height(self): return 0 @property - def depth(self): + def depth(self) -> int: """ Number of edges to the root `Node`. @@ -525,7 +542,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 +567,14 @@ def size(self): continue return size - def _pre_detach(self, parent): + def _pre_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent): + def _post_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent): + def _pre_attach(self, parent: NodeT | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent): + def _post_attach(self, parent: NodeT | None) -> None: """Method call after attaching to `parent`.""" diff --git a/anytree/node/symlinknode.py b/anytree/node/symlinknode.py index b90706d..7d0752e 100644 --- a/anytree/node/symlinknode.py +++ b/anytree/node/symlinknode.py @@ -1,9 +1,23 @@ # -*- 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 = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) + -class SymlinkNode(SymlinkNodeMixin): +class SymlinkNode(SymlinkNodeMixin, Generic[NodeT]): """ Tree node which references to another tree node. @@ -43,12 +57,18 @@ class SymlinkNode(SymlinkNodeMixin): 9 """ - def __init__(self, target, parent=None, children=None, **kwargs): + def __init__( + self, + target: NodeT, + parent: SymlinkNode[NodeT] | None = None, + children: Iterable[SymlinkNode[NodeT]] | 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..fa8b1f4 100644 --- a/anytree/node/symlinknodemixin.py +++ b/anytree/node/symlinknodemixin.py @@ -1,8 +1,11 @@ # -*- 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. @@ -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 [] From 7bfb258e7d097b65b9f05fd8ee86f63ef49460b3 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:38:38 -0600 Subject: [PATCH 3/9] Add mypy configuration Allows checking with mypy usign `mypy anytree` --- anytree/node/lightnodemixin.py | 4 ++-- anytree/node/nodemixin.py | 4 ++-- pyproject.toml | 22 +++++++++++++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/anytree/node/lightnodemixin.py b/anytree/node/lightnodemixin.py index 0fbc26c..4231c57 100644 --- a/anytree/node/lightnodemixin.py +++ b/anytree/node/lightnodemixin.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, Union from anytree.iterators import PreOrderIter @@ -14,7 +14,7 @@ from .nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) +NodeT = TypeVar("NodeT", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) class LightNodeMixin(Generic[NodeT]): diff --git a/anytree/node/nodemixin.py b/anytree/node/nodemixin.py index 2e6ae5f..8123571 100644 --- a/anytree/node/nodemixin.py +++ b/anytree/node/nodemixin.py @@ -3,7 +3,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Generic, TypeGuard, TypeVar, cast +from typing import TYPE_CHECKING, Any, Generic, TypeGuard, TypeVar, cast, Union from anytree.iterators import PreOrderIter @@ -15,7 +15,7 @@ from collections.abc import Generator, Iterable -NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) +NodeT = TypeVar("NodeT", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) class NodeMixin(Generic[NodeT]): 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 +""" From 35ebd2d1dc237b189f6e1a118c73d3f39cd1ee1f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:44:59 -0500 Subject: [PATCH 4/9] Add py.typed --- anytree/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 anytree/py.typed diff --git a/anytree/py.typed b/anytree/py.typed new file mode 100644 index 0000000..e69de29 From a182903742ddc8d77cf8e01c1957c011dc65320e Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:39:40 -0500 Subject: [PATCH 5/9] Tell typecheckers we are explicity exporting objects --- anytree/__init__.py | 64 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/anytree/__init__.py b/anytree/__init__.py index 5088206..e487d52 100644 --- a/anytree/__init__.py +++ b/anytree/__init__.py @@ -8,38 +8,38 @@ __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 +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 From b3139816ca925773fa7e3c6f15515e49c9bf8e0b Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:10:54 -0500 Subject: [PATCH 6/9] Fix runtime type issues --- anytree/iterators/abstractiter.py | 4 ++-- anytree/node/anynode.py | 2 +- anytree/node/node.py | 2 +- anytree/node/symlinknode.py | 2 +- anytree/node/symlinknodemixin.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/anytree/iterators/abstractiter.py b/anytree/iterators/abstractiter.py index fd6bfc0..f69a7dd 100644 --- a/anytree/iterators/abstractiter.py +++ b/anytree/iterators/abstractiter.py @@ -13,10 +13,10 @@ from ..node.nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) +NodeT = TypeVar("NodeT", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) -class AbstractIter(six.Iterator, Generic[NodeT]): +class AbstractIter(Generic[NodeT], six.Iterator): # pylint: disable=R0205 """ Iterate over tree starting at `node`. diff --git a/anytree/node/anynode.py b/anytree/node/anynode.py index 0e5ad00..dfe2880 100644 --- a/anytree/node/anynode.py +++ b/anytree/node/anynode.py @@ -11,7 +11,7 @@ from collections.abc import Iterable -class AnyNode(NodeMixin[AnyNode]): +class AnyNode(NodeMixin["AnyNode"]): """ A generic tree node with any `kwargs`. diff --git a/anytree/node/node.py b/anytree/node/node.py index efdebeb..c52eac8 100644 --- a/anytree/node/node.py +++ b/anytree/node/node.py @@ -11,7 +11,7 @@ from collections.abc import Iterable -class Node(NodeMixin[Node]): +class Node(NodeMixin["Node"]): """ A simple tree node with a `name` and any `kwargs`. diff --git a/anytree/node/symlinknode.py b/anytree/node/symlinknode.py index 7d0752e..91d12a6 100644 --- a/anytree/node/symlinknode.py +++ b/anytree/node/symlinknode.py @@ -14,7 +14,7 @@ from .nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound=NodeMixin[Any] | LightNodeMixin[Any], covariant=True) +NodeT = TypeVar("NodeT", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) class SymlinkNode(SymlinkNodeMixin, Generic[NodeT]): diff --git a/anytree/node/symlinknodemixin.py b/anytree/node/symlinknodemixin.py index fa8b1f4..88e550b 100644 --- a/anytree/node/symlinknodemixin.py +++ b/anytree/node/symlinknodemixin.py @@ -5,7 +5,7 @@ from .nodemixin import NodeMixin -class SymlinkNodeMixin(NodeMixin[SymlinkNodeMixin]): +class SymlinkNodeMixin(NodeMixin["SymlinkNodeMixin"]): """ The :any:`SymlinkNodeMixin` class extends any Python class to a symbolic link to a tree node. From c73c43dc2be248a47e2f5a064c203e41561dfa44 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:00:25 -0500 Subject: [PATCH 7/9] Fix pylint errors --- anytree/__init__.py | 1 + anytree/exporter/mermaidexporter.py | 1 - anytree/iterators/__init__.py | 1 + anytree/iterators/abstractiter.py | 29 ++++++----- anytree/node/__init__.py | 1 + anytree/node/lightnodemixin.py | 66 ++++++++++++------------- anytree/node/nodemixin.py | 74 +++++++++++++++-------------- anytree/node/symlinknode.py | 12 ++--- anytree/node/symlinknodemixin.py | 4 +- 9 files changed, 99 insertions(+), 90 deletions(-) diff --git a/anytree/__init__.py b/anytree/__init__.py index e487d52..c84bafd 100644 --- a/anytree/__init__.py +++ b/anytree/__init__.py @@ -8,6 +8,7 @@ __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 from . import util as util # noqa from .iterators import LevelOrderGroupIter as LevelOrderGroupIter # noqa 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 dc336bf..961104b 100644 --- a/anytree/iterators/__init__.py +++ b/anytree/iterators/__init__.py @@ -9,6 +9,7 @@ * :any:`ZigZagGroupIter`: iterate over tree using level-order strategy returning group for every level """ +# 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 diff --git a/anytree/iterators/abstractiter.py b/anytree/iterators/abstractiter.py index f69a7dd..960ae4c 100644 --- a/anytree/iterators/abstractiter.py +++ b/anytree/iterators/abstractiter.py @@ -13,10 +13,10 @@ from ..node.nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) +NodeT_co = TypeVar("NodeT_co", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) -class AbstractIter(Generic[NodeT], six.Iterator): +class AbstractIter(Generic[NodeT_co], six.Iterator): # pylint: disable=R0205 """ Iterate over tree starting at `node`. @@ -31,18 +31,18 @@ class AbstractIter(Generic[NodeT], six.Iterator): def __init__( self, - node: NodeT, - filter_: Callable[[NodeT], bool] | None = None, - stop: Callable[[NodeT], bool] | None = None, + 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: Iterator[NodeT] | None = None + self.__iter: Iterator[NodeT_co] | None = None - def __init(self) -> Iterator[NodeT]: + def __init(self) -> Iterator[NodeT_co]: node = self.node maxlevel = self.maxlevel filter_ = self.filter_ or AbstractIter.__default_filter @@ -51,27 +51,30 @@ def __init(self) -> Iterator[NodeT]: return self._iter(children, filter_, stop, maxlevel) @staticmethod - def __default_filter(node: NodeT) -> bool: + def __default_filter(node: NodeT_co) -> bool: # pylint: disable=W0613 return True @staticmethod - def __default_stop(node: NodeT) -> bool: + def __default_stop(node: NodeT_co) -> bool: # pylint: disable=W0613 return False def __iter__(self) -> Self: return self - def __next__(self) -> NodeT: + def __next__(self) -> NodeT_co: if self.__iter is None: self.__iter = self.__init() return next(self.__iter) @staticmethod def _iter( - children: Iterable[NodeT], filter_: Callable[[NodeT], bool], stop: Callable[[NodeT], bool], maxlevel: int | None - ) -> Iterator[NodeT]: + 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 @@ -79,5 +82,5 @@ def _abort_at_level(level: int, maxlevel: int | None) -> bool: return maxlevel is not None and level > maxlevel @staticmethod - def _get_children(children: Iterable[NodeT], stop: Callable[[NodeT], bool]) -> list[Any]: + 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 978b324..71b6319 100644 --- a/anytree/node/__init__.py +++ b/anytree/node/__init__.py @@ -9,6 +9,7 @@ * :any:`LightNodeMixin`: A :any:`NodeMixin` using slots. """ +# 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 diff --git a/anytree/node/lightnodemixin.py b/anytree/node/lightnodemixin.py index 4231c57..bfa37be 100644 --- a/anytree/node/lightnodemixin.py +++ b/anytree/node/lightnodemixin.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast, Union +from typing import TYPE_CHECKING, Generic, TypeVar, cast, Union from anytree.iterators import PreOrderIter @@ -10,15 +10,15 @@ from .exceptions import LoopError, TreeError if TYPE_CHECKING: + from typing_extensions import Any from collections.abc import Generator, Iterable from .nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) -class LightNodeMixin(Generic[NodeT]): - +class LightNodeMixin(Generic[NodeT_co]): """ The :any:`LightNodeMixin` behaves identical to :any:`NodeMixin`, but uses `__slots__`. @@ -97,7 +97,7 @@ class LightNodeMixin(Generic[NodeT]): separator = "/" @property - def parent(self) -> NodeT | None: + def parent(self) -> NodeT_co | None: """ Parent Node. @@ -137,7 +137,7 @@ def parent(self) -> NodeT | None: return None @parent.setter - def parent(self, value: NodeT | None) -> None: + def parent(self, value: NodeT_co | None) -> None: if hasattr(self, "_LightNodeMixin__parent"): parent = self.__parent else: @@ -147,7 +147,7 @@ def parent(self, value: NodeT | None) -> None: self.__detach(parent) self.__attach(value) - def __check_loop(self, node: NodeT | None) -> None: + 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." @@ -156,7 +156,7 @@ def __check_loop(self, node: NodeT | None) -> None: msg = "Cannot set parent. %r is parent of %r." raise LoopError(msg % (self, node)) - def __detach(self, parent: NodeT | None) -> None: + def __detach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -165,11 +165,11 @@ def __detach(self, parent: NodeT | None) -> None: 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: NodeT | None = None + self.__parent: NodeT_co | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent: NodeT | None) -> None: + def __attach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -183,16 +183,16 @@ def __attach(self, parent: NodeT | None) -> None: self._post_attach(parent) @property - def __children_or_empty(self) -> list[NodeT]: + def __children_or_empty(self) -> list[NodeT_co]: if not hasattr(self, "_LightNodeMixin__children"): - self.__children: list[NodeT] = [] + self.__children: list[NodeT_co] = [] return self.__children - def __children_get(self) -> tuple[NodeT, ...]: + def __children_get(self) -> tuple[NodeT_co, ...]: return tuple(self.__children_or_empty) @staticmethod - def __check_children(children: Iterable[NodeT]) -> None: + def __check_children(children: Iterable[NodeT_co]) -> None: seen = set() for child in children: childid = id(child) @@ -202,7 +202,7 @@ def __check_children(children: Iterable[NodeT]) -> None: msg = "Cannot add node %r multiple times as child." % (child,) raise TreeError(msg) - def __children_set(self, children: Iterable[NodeT]) -> None: + def __children_set(self, children: Iterable[NodeT_co]) -> None: # convert iterable to tuple children = tuple(children) LightNodeMixin.__check_children(children) @@ -283,20 +283,20 @@ def __children_del(self) -> None: """, ) - def _pre_detach_children(self, children: tuple[NodeT, ...]) -> None: + def _pre_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children: tuple[NodeT, ...]) -> None: + def _post_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children: tuple[NodeT, ...]) -> None: + def _pre_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children: tuple[NodeT, ...]) -> None: + def _post_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self) -> tuple[NodeT, ...]: + def path(self) -> tuple[NodeT_co, ...]: """ Path from root node down to this `Node`. @@ -313,7 +313,7 @@ def path(self) -> tuple[NodeT, ...]: """ return self._path - def iter_path_reverse(self) -> Generator[NodeT, None, None]: + def iter_path_reverse(self) -> Generator[NodeT_co, None, None]: """ Iterate up the tree from the current node to the root node. @@ -334,17 +334,17 @@ def iter_path_reverse(self) -> Generator[NodeT, None, None]: Node('/Udo/Marc') Node('/Udo') """ - node: NodeT | None = cast(NodeT, self) + node: NodeT_co | None = cast(NodeT_co, self) while node is not None: yield node node = node.parent @property - def _path(self) -> tuple[NodeT, ...]: + def _path(self) -> tuple[NodeT_co, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self) -> tuple[NodeT, ...]: + def ancestors(self) -> tuple[NodeT_co, ...]: """ All parent nodes and their parent nodes. @@ -364,7 +364,7 @@ def ancestors(self) -> tuple[NodeT, ...]: return self.parent.path @property - def descendants(self) -> tuple[NodeT, ...]: + def descendants(self) -> tuple[NodeT_co, ...]: """ All child nodes and all their child nodes. @@ -384,7 +384,7 @@ def descendants(self) -> tuple[NodeT, ...]: return tuple(PreOrderIter(self))[1:] @property - def root(self) -> NodeT: + def root(self) -> NodeT_co: """ Tree Root Node. @@ -399,13 +399,13 @@ def root(self) -> NodeT: >>> lian.root Node('/Udo') """ - node: NodeT = cast(NodeT, self) + node: NodeT_co = cast(NodeT_co, self) while node.parent is not None: node = node.parent return node @property - def siblings(self) -> tuple[NodeT, ...]: + def siblings(self) -> tuple[NodeT_co, ...]: """ Tuple of nodes with the same parent. @@ -430,7 +430,7 @@ def siblings(self) -> tuple[NodeT, ...]: return tuple(node for node in parent.children if node is not self) @property - def leaves(self) -> tuple[NodeT, ...]: + def leaves(self) -> tuple[NodeT_co, ...]: """ Tuple of all leaf nodes. @@ -552,14 +552,14 @@ def size(self) -> int: continue return size - def _pre_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: + def _pre_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: + def _post_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent: NodeT | None) -> None: + def _pre_attach(self, parent: NodeT_co | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent: NodeT | None) -> None: + def _post_attach(self, parent: NodeT_co | None) -> None: """Method call after attaching to `parent`.""" diff --git a/anytree/node/nodemixin.py b/anytree/node/nodemixin.py index 8123571..0bdcd62 100644 --- a/anytree/node/nodemixin.py +++ b/anytree/node/nodemixin.py @@ -3,7 +3,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Generic, TypeGuard, TypeVar, cast, Union +from typing import TYPE_CHECKING, Generic, TypeVar, cast, Union from anytree.iterators import PreOrderIter @@ -12,14 +12,15 @@ from .lightnodemixin import LightNodeMixin if TYPE_CHECKING: - from collections.abc import Generator, Iterable + from typing_extensions import TypeGuard, Any + from collections.abc import Generator, Iterable -NodeT = TypeVar("NodeT", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) +NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) -class NodeMixin(Generic[NodeT]): +class NodeMixin(Generic[NodeT_co]): """ The :any:`NodeMixin` class extends any Python class to a tree node. @@ -90,7 +91,7 @@ class NodeMixin(Generic[NodeT]): separator = "/" @property - def parent(self) -> NodeT | None: + def parent(self) -> NodeT_co | None: """ Parent Node. @@ -131,7 +132,7 @@ def parent(self) -> NodeT | None: @parent.setter def parent(self, value: object | None) -> None: - def guard(value: object | None) -> TypeGuard[NodeT | None]: + def guard(value: object | None) -> TypeGuard[NodeT_co | None]: return value is None or isinstance(value, (NodeMixin, LightNodeMixin)) if not guard(value): @@ -146,7 +147,7 @@ def guard(value: object | None) -> TypeGuard[NodeT | None]: self.__detach(parent) self.__attach(value) - def __check_loop(self, node: NodeT | None) -> None: + 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." @@ -155,7 +156,7 @@ def __check_loop(self, node: NodeT | None) -> None: msg = "Cannot set parent. %r is parent of %r." raise LoopError(msg % (self, node)) - def __detach(self, parent: NodeT | None) -> None: + def __detach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212,W0238 if parent is not None: self._pre_detach(parent) @@ -164,11 +165,11 @@ def __detach(self, parent: NodeT | None) -> None: 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: NodeT | None = None + self.__parent: NodeT_co | None = None # ATOMIC END self._post_detach(parent) - def __attach(self, parent: NodeT | None) -> None: + def __attach(self, parent: NodeT_co | None) -> None: # pylint: disable=W0212 if parent is not None: self._pre_attach(parent) @@ -182,12 +183,12 @@ def __attach(self, parent: NodeT | None) -> None: self._post_attach(parent) @property - def __children_or_empty(self) -> list[NodeT]: + def __children_or_empty(self) -> list[NodeT_co]: if not hasattr(self, "_NodeMixin__children"): - self.__children: list[NodeT] = [] + self.__children: list[NodeT_co] = [] return self.__children - def __children_get(self) -> tuple[NodeT, ...]: + def __children_get(self) -> tuple[NodeT_co, ...]: return tuple(self.__children_or_empty) @staticmethod @@ -206,7 +207,7 @@ def __check_children(children: Iterable[object]) -> None: msg = "Cannot add node %r multiple times as child." % (child,) raise TreeError(msg) - def __children_set(self, children: Iterable[NodeT]) -> None: + def __children_set(self, children: Iterable[NodeT_co]) -> None: # convert iterable to tuple children = tuple(children) NodeMixin.__check_children(children) @@ -287,20 +288,20 @@ def __children_del(self) -> None: """, ) - def _pre_detach_children(self, children: tuple[NodeT, ...]) -> None: + def _pre_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before detaching `children`.""" - def _post_detach_children(self, children: tuple[NodeT, ...]) -> None: + def _post_detach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after detaching `children`.""" - def _pre_attach_children(self, children: tuple[NodeT, ...]) -> None: + def _pre_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call before attaching `children`.""" - def _post_attach_children(self, children: tuple[NodeT, ...]) -> None: + def _post_attach_children(self, children: tuple[NodeT_co, ...]) -> None: """Method call after attaching `children`.""" @property - def path(self) -> tuple[NodeT, ...]: + def path(self) -> tuple[NodeT_co, ...]: """ Path from root node down to this `Node`. @@ -317,7 +318,7 @@ def path(self) -> tuple[NodeT, ...]: """ return self._path - def iter_path_reverse(self) -> Generator[NodeT, None, None]: + def iter_path_reverse(self) -> Generator[NodeT_co, None, None]: """ Iterate up the tree from the current node to the root node. @@ -338,17 +339,17 @@ def iter_path_reverse(self) -> Generator[NodeT, None, None]: Node('/Udo/Marc') Node('/Udo') """ - node: NodeT | None = cast(NodeT, self) + node: NodeT_co | None = cast(NodeT_co, self) while node is not None: yield node node = node.parent @property - def _path(self) -> tuple[NodeT, ...]: + def _path(self) -> tuple[NodeT_co, ...]: return tuple(reversed(list(self.iter_path_reverse()))) @property - def ancestors(self) -> tuple[NodeT, ...]: + def ancestors(self) -> tuple[NodeT_co, ...]: """ All parent nodes and their parent nodes. @@ -368,18 +369,21 @@ def ancestors(self) -> tuple[NodeT, ...]: return self.parent.path @property - def anchestors(self) -> tuple[NodeT, ...]: + 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) -> tuple[NodeT, ...]: + def descendants(self) -> tuple[NodeT_co, ...]: """ All child nodes and all their child nodes. @@ -399,7 +403,7 @@ def descendants(self) -> tuple[NodeT, ...]: return tuple(PreOrderIter(self))[1:] @property - def root(self) -> NodeT: + def root(self) -> NodeT_co: """ Tree Root Node. @@ -414,13 +418,13 @@ def root(self) -> NodeT: >>> lian.root Node('/Udo') """ - node: NodeT = cast(NodeT, self) + node: NodeT_co = cast(NodeT_co, self) while node.parent is not None: node = node.parent return node @property - def siblings(self) -> tuple[NodeT, ...]: + def siblings(self) -> tuple[NodeT_co, ...]: """ Tuple of nodes with the same parent. @@ -445,7 +449,7 @@ def siblings(self) -> tuple[NodeT, ...]: return tuple(node for node in parent.children if node is not self) @property - def leaves(self) -> tuple[NodeT, ...]: + def leaves(self) -> tuple[NodeT_co, ...]: """ Tuple of all leaf nodes. @@ -567,14 +571,14 @@ def size(self) -> int: continue return size - def _pre_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: + def _pre_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call before detaching from `parent`.""" - def _post_detach(self, parent: NodeMixin[NodeT] | LightNodeMixin[NodeT]) -> None: + def _post_detach(self, parent: NodeMixin[NodeT_co] | LightNodeMixin[NodeT_co]) -> None: """Method call after detaching from `parent`.""" - def _pre_attach(self, parent: NodeT | None) -> None: + def _pre_attach(self, parent: NodeT_co | None) -> None: """Method call before attaching to `parent`.""" - def _post_attach(self, parent: NodeT | None) -> None: + 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 91d12a6..3987cef 100644 --- a/anytree/node/symlinknode.py +++ b/anytree/node/symlinknode.py @@ -14,15 +14,15 @@ from .nodemixin import NodeMixin -NodeT = TypeVar("NodeT", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) +NodeT_co = TypeVar("NodeT_co", bound="NodeMixin[Any] | LightNodeMixin[Any]", covariant=True) -class SymlinkNode(SymlinkNodeMixin, Generic[NodeT]): +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. @@ -59,9 +59,9 @@ class SymlinkNode(SymlinkNodeMixin, Generic[NodeT]): def __init__( self, - target: NodeT, - parent: SymlinkNode[NodeT] | None = None, - children: Iterable[SymlinkNode[NodeT]] | None = None, + target: NodeT_co, + parent: SymlinkNode[NodeT_co] | None = None, + children: Iterable[SymlinkNode[NodeT_co]] | None = None, **kwargs: Any, ) -> None: self.target = target diff --git a/anytree/node/symlinknodemixin.py b/anytree/node/symlinknodemixin.py index 88e550b..4d781df 100644 --- a/anytree/node/symlinknodemixin.py +++ b/anytree/node/symlinknodemixin.py @@ -9,10 +9,10 @@ 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): From 5f45e9587df47ccf437b6fe52c1235aadfb846b6 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:10:27 -0500 Subject: [PATCH 8/9] Re-run isort again --- anytree/node/lightnodemixin.py | 5 +++-- anytree/node/nodemixin.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/anytree/node/lightnodemixin.py b/anytree/node/lightnodemixin.py index bfa37be..70a9acd 100644 --- a/anytree/node/lightnodemixin.py +++ b/anytree/node/lightnodemixin.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generic, TypeVar, cast, Union +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast from anytree.iterators import PreOrderIter @@ -10,9 +10,10 @@ from .exceptions import LoopError, TreeError if TYPE_CHECKING: - from typing_extensions import Any 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) diff --git a/anytree/node/nodemixin.py b/anytree/node/nodemixin.py index 0bdcd62..6222cc2 100644 --- a/anytree/node/nodemixin.py +++ b/anytree/node/nodemixin.py @@ -3,7 +3,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Generic, TypeVar, cast, Union +from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast from anytree.iterators import PreOrderIter @@ -12,10 +12,10 @@ from .lightnodemixin import LightNodeMixin if TYPE_CHECKING: - from typing_extensions import TypeGuard, Any - from collections.abc import Generator, Iterable + from typing_extensions import Any, TypeGuard + NodeT_co = TypeVar("NodeT_co", bound=Union["NodeMixin[Any]", "LightNodeMixin[Any]"], covariant=True) From 19150ad6c64a36faca06ba43b3c50e56542f74d0 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:21:06 -0500 Subject: [PATCH 9/9] Update expected assertion errors --- tests/test_node.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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)]