1
0
Fork 0

Merging upstream version 1.7.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-03-05 09:29:32 +01:00
parent 9b4cf108f9
commit 49565454d8
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
7 changed files with 235 additions and 148 deletions

View file

@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v5.4.0
with: with:
python-version: '3.x' python-version: '3.x'
- name: Install dependencies - name: Install dependencies

View file

@ -6,21 +6,38 @@ on:
push: push:
branches: [ "master", "dev" ] branches: [ "master", "dev" ]
pull_request: pull_request:
branches: [ "master" ] types: [opened, synchronize, reopened]
jobs: jobs:
build: build-compat:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12.0-rc.1"] python-version: ["3.6", "3.7"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@v5.4.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f requirements-t-pre37.txt ]; then pip install -r requirements-t-pre37.txt; fi
- name: Test with nosetest
run: |
./scripts/testing.sh
build-latest:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5.4.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View file

@ -1,15 +0,0 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "3.7"
- "3.8"
install:
- "python setup.py install"
script:
- pip install -r requirements-testing.txt
- sh testing.sh

View file

@ -1,6 +1,6 @@
from setuptools import setup from setuptools import setup
__version__ = "1.7.0" __version__ = "1.7.1"
setup( setup(
@ -15,6 +15,8 @@ setup(
packages=["treelib"], packages=["treelib"],
keywords=["data structure", "tree", "tools"], keywords=["data structure", "tree", "tools"],
install_requires=["six"], install_requires=["six"],
include_package_data=True,
package_data={"": ["py.typed"]},
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
@ -23,8 +25,6 @@ setup(
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
@ -32,6 +32,7 @@ setup(
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
], ],
) )

View file

@ -27,12 +27,19 @@ from __future__ import unicode_literals
import copy import copy
import uuid import uuid
import sys
from collections import defaultdict from collections import defaultdict
from warnings import warn from warnings import warn
from typing import cast, List, Any, Optional, Union
from .exceptions import NodePropertyError from .exceptions import NodePropertyError
from .misc import deprecated from .misc import deprecated
if sys.version_info >= (3, 9):
StrList = list[str]
else:
StrList = List[str] # Python 3.8 and earlier
class Node(object): class Node(object):
""" """
@ -43,11 +50,17 @@ class Node(object):
#: Mode constants for routine `update_fpointer()`. #: Mode constants for routine `update_fpointer()`.
(ADD, DELETE, INSERT, REPLACE) = list(range(4)) (ADD, DELETE, INSERT, REPLACE) = list(range(4))
def __init__(self, tag=None, identifier=None, expanded=True, data=None): def __init__(
self,
tag: Optional[str] = None,
identifier: Optional[str] = None,
expanded: bool = True,
data: Any = None,
) -> None:
"""Create a new Node object to be placed inside a Tree object""" """Create a new Node object to be placed inside a Tree object"""
#: if given as a parameter, must be unique #: if given as a parameter, must be unique
self._identifier = None self._identifier: Optional[str] = None
self._set_identifier(identifier) self._set_identifier(identifier)
#: None or something else #: None or something else
@ -61,24 +74,24 @@ class Node(object):
self.expanded = expanded self.expanded = expanded
#: identifier of the parent's node : #: identifier of the parent's node :
self._predecessor = {} self._predecessor: dict = {}
#: identifier(s) of the soons' node(s) : #: identifier(s) of the soons' node(s) :
self._successors = defaultdict(list) self._successors: dict = defaultdict(list)
#: User payload associated with this node. #: User payload associated with this node.
self.data = data self.data = data
# for retro-compatibility on bpointer/fpointer # for retro-compatibility on bpointer/fpointer
self._initial_tree_id = None self._initial_tree_id: Optional[str] = None
def __lt__(self, other): def __lt__(self, other) -> bool:
return self.tag < other.tag return self.tag < other.tag
def set_initial_tree_id(self, tree_id): def set_initial_tree_id(self, tree_id: str) -> None:
if self._initial_tree_id is None: if self._initial_tree_id is None:
self._initial_tree_id = tree_id self._initial_tree_id = tree_id
def _set_identifier(self, nid): def _set_identifier(self, nid: Optional[str]) -> None:
"""Initialize self._set_identifier""" """Initialize self._set_identifier"""
if nid is None: if nid is None:
self._identifier = str(uuid.uuid1()) self._identifier = str(uuid.uuid1())
@ -98,11 +111,11 @@ class Node(object):
@bpointer.setter @bpointer.setter
@deprecated(alias="node.set_predecessor") @deprecated(alias="node.set_predecessor")
def bpointer(self, value): def bpointer(self, value) -> None:
self.set_predecessor(value, self._initial_tree_id) self.set_predecessor(value, self._initial_tree_id)
@deprecated(alias="node.set_predecessor") @deprecated(alias="node.set_predecessor")
def update_bpointer(self, nid): def update_bpointer(self, nid) -> None:
self.set_predecessor(nid, self._initial_tree_id) self.set_predecessor(nid, self._initial_tree_id)
@property @property
@ -118,7 +131,7 @@ class Node(object):
@fpointer.setter @fpointer.setter
@deprecated(alias="node.update_successors") @deprecated(alias="node.update_successors")
def fpointer(self, value): def fpointer(self, value: Union[None, list, dict, set]) -> None:
self.set_successors(value, tree_id=self._initial_tree_id) self.set_successors(value, tree_id=self._initial_tree_id)
@deprecated(alias="node.update_successors") @deprecated(alias="node.update_successors")
@ -132,11 +145,11 @@ class Node(object):
""" """
return self._predecessor[tree_id] return self._predecessor[tree_id]
def set_predecessor(self, nid, tree_id): def set_predecessor(self, nid: Optional[str], tree_id: Optional[str]) -> None:
"""Set the value of `_predecessor`.""" """Set the value of `_predecessor`."""
self._predecessor[tree_id] = nid self._predecessor[tree_id] = nid
def successors(self, tree_id): def successors(self, tree_id: Optional[str]) -> StrList:
""" """
With a getting operator, a list of IDs of node's children is obtained. With With a getting operator, a list of IDs of node's children is obtained. With
a setting operator, the value can be list, set, or dict. For list or set, a setting operator, the value can be list, set, or dict. For list or set,
@ -145,7 +158,9 @@ class Node(object):
""" """
return self._successors[tree_id] return self._successors[tree_id]
def set_successors(self, value, tree_id=None): def set_successors(
self, value: Union[None, list, dict, set], tree_id: Optional[str] = None
) -> None:
"""Set the value of `_successors`.""" """Set the value of `_successors`."""
setter_lookup = { setter_lookup = {
"NoneType": lambda x: list(), "NoneType": lambda x: list(),
@ -161,7 +176,13 @@ class Node(object):
else: else:
raise NotImplementedError("Unsupported value type %s" % t) raise NotImplementedError("Unsupported value type %s" % t)
def update_successors(self, nid, mode=ADD, replace=None, tree_id=None): def update_successors(
self,
nid: Optional[str],
mode: int = ADD,
replace: Optional[str] = None,
tree_id: Optional[str] = None,
) -> None:
""" """
Update the children list with different modes: addition (Node.ADD or Update the children list with different modes: addition (Node.ADD or
Node.INSERT) and deletion (Node.DELETE). Node.INSERT) and deletion (Node.DELETE).
@ -169,23 +190,23 @@ class Node(object):
if nid is None: if nid is None:
return return
def _manipulator_append(): def _manipulator_append() -> None:
self.successors(tree_id).append(nid) self.successors(tree_id).append(nid)
def _manipulator_delete(): def _manipulator_delete() -> None:
if nid in self.successors(tree_id): if nid in self.successors(tree_id):
self.successors(tree_id).remove(nid) self.successors(tree_id).remove(nid)
else: else:
warn("Nid %s wasn't present in fpointer" % nid) warn("Nid %s wasn't present in fpointer" % nid)
def _manipulator_insert(): def _manipulator_insert() -> None:
warn("WARNING: INSERT is deprecated to ADD mode") warn("WARNING: INSERT is deprecated to ADD mode")
self.update_successors(nid, tree_id=tree_id) self.update_successors(nid, tree_id=tree_id)
def _manipulator_replace(): def _manipulator_replace() -> None:
if replace is None: if replace is None:
raise NodePropertyError( raise NodePropertyError(
'Argument "repalce" should be provided when mode is {}'.format(mode) 'Argument "replace" should be provided when mode is {}'.format(mode)
) )
ind = self.successors(tree_id).index(nid) ind = self.successors(tree_id).index(nid)
self.successors(tree_id)[ind] = replace self.successors(tree_id)[ind] = replace
@ -200,38 +221,38 @@ class Node(object):
if mode not in manipulator_lookup: if mode not in manipulator_lookup:
raise NotImplementedError("Unsupported node updating mode %s" % str(mode)) raise NotImplementedError("Unsupported node updating mode %s" % str(mode))
f_name = manipulator_lookup.get(mode) f_name = cast(str, manipulator_lookup.get(mode))
f = locals()[f_name] f = locals()[f_name]
return f() return f()
@property @property
def identifier(self): def identifier(self) -> str:
""" """
The unique ID of a node within the scope of a tree. This attribute can be The unique ID of a node within the scope of a tree. This attribute can be
accessed and modified with ``.`` and ``=`` operator respectively. accessed and modified with ``.`` and ``=`` operator respectively.
""" """
return self._identifier return cast(str, self._identifier)
def clone_pointers(self, former_tree_id, new_tree_id): def clone_pointers(self, former_tree_id: str, new_tree_id: str) -> None:
former_bpointer = self.predecessor(former_tree_id) former_bpointer = self.predecessor(former_tree_id)
self.set_predecessor(former_bpointer, new_tree_id) self.set_predecessor(former_bpointer, new_tree_id)
former_fpointer = self.successors(former_tree_id) former_fpointer = self.successors(former_tree_id)
# fpointer is a list and would be copied by reference without deepcopy # fpointer is a list and would be copied by reference without deepcopy
self.set_successors(copy.deepcopy(former_fpointer), tree_id=new_tree_id) self.set_successors(copy.deepcopy(former_fpointer), tree_id=new_tree_id)
def reset_pointers(self, tree_id): def reset_pointers(self, tree_id) -> None:
self.set_predecessor(None, tree_id) self.set_predecessor(None, tree_id)
self.set_successors([], tree_id=tree_id) self.set_successors([], tree_id=tree_id)
@identifier.setter @identifier.setter # type: ignore
def identifier(self, value): def identifier(self, value: str) -> None:
"""Set the value of `_identifier`.""" """Set the value of `_identifier`."""
if value is None: if value is None:
print("WARNING: node ID can not be None") print("WARNING: node ID can not be None")
else: else:
self._set_identifier(value) self._set_identifier(value)
def is_leaf(self, tree_id=None): def is_leaf(self, tree_id: Optional[str] = None) -> bool:
"""Return true if current node has no children.""" """Return true if current node has no children."""
if tree_id is None: if tree_id is None:
# for retro-compatilibity # for retro-compatilibity
@ -245,7 +266,7 @@ class Node(object):
else: else:
return False return False
def is_root(self, tree_id=None): def is_root(self, tree_id: Optional[str] = None) -> bool:
"""Return true if self has no parent, i.e. as root.""" """Return true if self has no parent, i.e. as root."""
if tree_id is None: if tree_id is None:
# for retro-compatilibity # for retro-compatilibity
@ -257,19 +278,19 @@ class Node(object):
return self.predecessor(tree_id) is None return self.predecessor(tree_id) is None
@property @property
def tag(self): def tag(self) -> str:
""" """
The readable node name for human. This attribute can be accessed and The readable node name for human. This attribute can be accessed and
modified with ``.`` and ``=`` operator respectively. modified with ``.`` and ``=`` operator respectively.
""" """
return self._tag return cast(str, self._tag)
@tag.setter @tag.setter
def tag(self, value): def tag(self, value: Optional[str]) -> None:
"""Set the value of `_tag`.""" """Set the value of `_tag`."""
self._tag = value if value is not None else None self._tag = value if value is not None else None
def __repr__(self): def __repr__(self) -> str:
name = self.__class__.__name__ name = self.__class__.__name__
kwargs = [ kwargs = [
"tag={0}".format(self.tag), "tag={0}".format(self.tag),

0
treelib/py.typed Normal file
View file

View file

@ -32,16 +32,18 @@ from __future__ import unicode_literals
try: try:
from builtins import str as text from builtins import str as text
except ImportError: except ImportError:
from __builtin__ import str as text from __builtin__ import str as text # type: ignore
import codecs import codecs
import json import json
import uuid import uuid
import sys
from copy import deepcopy from copy import deepcopy
from six import python_2_unicode_compatible, iteritems from six import python_2_unicode_compatible, iteritems
from typing import cast, List, Any, Callable, Optional, Union
try: try:
from StringIO import StringIO from StringIO import StringIO # type: ignore
except ImportError: except ImportError:
from io import StringIO from io import StringIO
@ -55,6 +57,16 @@ from .exceptions import (
) )
from .node import Node from .node import Node
if sys.version_info >= (3, 9):
StrList = list[str]
StrLList = list[list[str]]
NodeList = list[Node]
else:
StrList = List[str] # Python 3.8 and earlier
StrLList = List[List[str]]
NodeList = List[Node]
__author__ = "chenxm" __author__ = "chenxm"
@ -66,14 +78,20 @@ class Tree(object):
(ROOT, DEPTH, WIDTH, ZIGZAG) = list(range(4)) (ROOT, DEPTH, WIDTH, ZIGZAG) = list(range(4))
node_class = Node node_class = Node
def __contains__(self, identifier): def __contains__(self, identifier: str) -> bool:
return identifier in self.nodes.keys() return identifier in self.nodes.keys()
def __init__(self, tree=None, deep=False, node_class=None, identifier=None): def __init__(
self,
tree=None,
deep: bool = False,
node_class=None,
identifier: Optional[str] = None,
) -> None:
"""Initiate a new tree or copy another tree with a shallow or """Initiate a new tree or copy another tree with a shallow or
deep copy. deep copy.
""" """
self._identifier = None self._identifier: Optional[str] = None
self._set_identifier(identifier) self._set_identifier(identifier)
if node_class: if node_class:
@ -85,7 +103,7 @@ class Tree(object):
#: Get or set the identifier of the root. This attribute can be accessed and modified #: Get or set the identifier of the root. This attribute can be accessed and modified
#: with ``.`` and ``=`` operator respectively. #: with ``.`` and ``=`` operator respectively.
self.root = None self.root: Optional[str] = None
if tree is not None: if tree is not None:
self.root = tree.root self.root = tree.root
@ -95,7 +113,12 @@ class Tree(object):
if tree.identifier != self._identifier: if tree.identifier != self._identifier:
new_node.clone_pointers(tree.identifier, self._identifier) new_node.clone_pointers(tree.identifier, self._identifier)
def _clone(self, identifier=None, with_tree=False, deep=False): def _clone(
self,
identifier: Optional[str] = None,
with_tree: bool = False,
deep: bool = False,
):
"""Clone current instance, with or without tree. """Clone current instance, with or without tree.
Method intended to be overloaded, to avoid rewriting whole "subtree" and "remove_subtree" methods when Method intended to be overloaded, to avoid rewriting whole "subtree" and "remove_subtree" methods when
@ -122,28 +145,28 @@ class Tree(object):
) )
@property @property
def identifier(self): def identifier(self) -> Optional[str]:
return self._identifier return self._identifier
def _set_identifier(self, nid): def _set_identifier(self, nid: Optional[str]) -> None:
"""Initialize self._set_identifier""" """Initialize self._set_identifier"""
if nid is None: if nid is None:
self._identifier = str(uuid.uuid1()) self._identifier = str(uuid.uuid1())
else: else:
self._identifier = nid self._identifier = nid
def __getitem__(self, key): def __getitem__(self, key: str) -> Node:
"""Return _nodes[key]""" """Return _nodes[key]"""
try: try:
return self._nodes[key] return self._nodes[key]
except KeyError: except KeyError:
raise NodeIDAbsentError("Node '%s' is not in the tree" % key) raise NodeIDAbsentError("Node '%s' is not in the tree" % key)
def __len__(self): def __len__(self) -> int:
"""Return len(_nodes)""" """Return len(_nodes)"""
return len(self._nodes) return len(self._nodes)
def __str__(self): def __str__(self) -> str:
self._reader = "" self._reader = ""
def write(line): def write(line):
@ -154,16 +177,16 @@ class Tree(object):
def __print_backend( def __print_backend(
self, self,
nid=None, nid: Optional[str] = None,
level=ROOT, level: int = ROOT,
idhidden=True, idhidden: bool = True,
filter=None, filter: Optional[Callable[[Node], bool]] = None,
key=None, key: Optional[Callable[[Node], Node]] = None,
reverse=False, reverse: bool = False,
line_type="ascii-ex", line_type="ascii-ex",
data_property=None, data_property=None,
sorting=True, sorting: bool = True,
func=print, func: Callable[[bytes], None] = print,
): ):
""" """
Another implementation of printing tree using Stack Another implementation of printing tree using Stack
@ -193,12 +216,12 @@ class Tree(object):
if data_property: if data_property:
if idhidden: if idhidden:
def get_label(node): def get_label(node: Node) -> str:
return getattr(node.data, data_property) return getattr(node.data, data_property)
else: else:
def get_label(node): def get_label(node: Node) -> str:
return "%s[%s]" % ( return "%s[%s]" % (
getattr(node.data, data_property), getattr(node.data, data_property),
node.identifier, node.identifier,
@ -207,12 +230,12 @@ class Tree(object):
else: else:
if idhidden: if idhidden:
def get_label(node): def get_label(node: Node) -> str:
return node.tag return node.tag
else: else:
def get_label(node): def get_label(node: Node) -> str:
return "%s[%s]" % (node.tag, node.identifier) return "%s[%s]" % (node.tag, node.identifier)
# legacy ordering # legacy ordering
@ -228,7 +251,16 @@ class Tree(object):
label = get_label(node) label = get_label(node)
func("{0}{1}".format(pre, label).encode("utf-8")) func("{0}{1}".format(pre, label).encode("utf-8"))
def __get(self, nid, level, filter_, key, reverse, line_type, sorting): def __get(
self,
nid: Optional[str],
level: int,
filter_: Optional[Callable[[Node], bool]],
key: Optional[Callable[[Node], Node]],
reverse: bool,
line_type: str,
sorting: bool,
):
# default filter # default filter
if filter_ is None: if filter_ is None:
@ -247,10 +279,20 @@ class Tree(object):
return self.__get_iter(nid, level, filter_, key, reverse, dt, [], sorting) return self.__get_iter(nid, level, filter_, key, reverse, dt, [], sorting)
def __get_iter(self, nid, level, filter_, key, reverse, dt, is_last, sorting): def __get_iter(
self,
nid: Optional[str],
level: int,
filter_: Callable[[Node], bool],
key: Optional[Callable[[Node], Node]],
reverse: bool,
dt, # tuple[str, str, str]
is_last: list,
sorting: bool,
):
dt_vertical_line, dt_line_box, dt_line_corner = dt dt_vertical_line, dt_line_box, dt_line_corner = dt
nid = self.root if nid is None else nid nid = cast(str, self.root if nid is None else nid)
node = self[nid] node = self[nid]
if level == self.ROOT: if level == self.ROOT:
@ -274,7 +316,7 @@ class Tree(object):
if key: if key:
children.sort(key=key, reverse=reverse) children.sort(key=key, reverse=reverse)
elif reverse: elif reverse:
children = reversed(children) children = list(reversed(children))
level += 1 level += 1
for idx, child in enumerate(children): for idx, child in enumerate(children):
is_last.append(idx == idxlast) is_last.append(idx == idxlast)
@ -288,13 +330,13 @@ class Tree(object):
"""set self[nid].bpointer""" """set self[nid].bpointer"""
self[nid].set_predecessor(parent_id, self._identifier) self[nid].set_predecessor(parent_id, self._identifier)
def __update_fpointer(self, nid, child_id, mode): def __update_fpointer(self, nid: str, child_id: str, mode: int):
if nid is None: if nid is None:
return return
else: else:
self[nid].update_successors(child_id, mode, tree_id=self._identifier) self[nid].update_successors(child_id, mode, tree_id=self._identifier)
def add_node(self, node, parent=None): def add_node(self, node: Node, parent: Optional[Union[Node, str]] = None) -> None:
""" """
Add a new node object to the tree and make the parent as the root by default. Add a new node object to the tree and make the parent as the root by default.
@ -320,16 +362,17 @@ class Tree(object):
elif not self.contains(pid): elif not self.contains(pid):
raise NodeIDAbsentError("Parent node '%s' " "is not in the tree" % pid) raise NodeIDAbsentError("Parent node '%s' " "is not in the tree" % pid)
pid = cast(str, pid)
self._nodes.update({node.identifier: node}) self._nodes.update({node.identifier: node})
self.__update_fpointer(pid, node.identifier, self.node_class.ADD) self.__update_fpointer(pid, node.identifier, self.node_class.ADD)
self.__update_bpointer(node.identifier, pid) self.__update_bpointer(node.identifier, pid)
node.set_initial_tree_id(self._identifier) node.set_initial_tree_id(cast(str, self._identifier))
def all_nodes(self): def all_nodes(self) -> NodeList:
"""Return all nodes in a list""" """Return all nodes in a list"""
return list(self._nodes.values()) return list(self._nodes.values())
def all_nodes_itr(self): def all_nodes_itr(self) -> Any:
""" """
Returns all nodes in an iterator. Returns all nodes in an iterator.
Added by William Rusnack Added by William Rusnack
@ -368,7 +411,7 @@ class Tree(object):
ascendant_level = self.level(ascendant) ascendant_level = self.level(ascendant)
return None return None
def children(self, nid): def children(self, nid: str) -> NodeList:
""" """
Return the children (Node) list of nid. Return the children (Node) list of nid.
Empty list is returned if nid does not exist Empty list is returned if nid does not exist
@ -379,7 +422,13 @@ class Tree(object):
"""Check if the tree contains node of given id""" """Check if the tree contains node of given id"""
return True if nid in self._nodes else False return True if nid in self._nodes else False
def create_node(self, tag=None, identifier=None, parent=None, data=None): def create_node(
self,
tag: Optional[str] = None,
identifier: Optional[str] = None,
parent: Optional[Union[Node, str]] = None,
data: Any = None,
) -> Node:
""" """
Create a child node for given @parent node. If ``identifier`` is absent, Create a child node for given @parent node. If ``identifier`` is absent,
a UUID will be generated automatically. a UUID will be generated automatically.
@ -388,7 +437,7 @@ class Tree(object):
self.add_node(node, parent) self.add_node(node, parent)
return node return node
def depth(self, node=None): def depth(self, node: Optional[Node] = None) -> int:
""" """
Get the maximum level of this tree or the level of the given node. Get the maximum level of this tree or the level of the given node.
@ -415,7 +464,13 @@ class Tree(object):
return ret return ret
def expand_tree( def expand_tree(
self, nid=None, mode=DEPTH, filter=None, key=None, reverse=False, sorting=True self,
nid: Optional[str] = None,
mode: int = DEPTH,
filter: Optional[Callable[[Node], bool]] = None,
key: Optional[Callable[[Node], Node]] = None,
reverse: bool = False,
sorting: bool = True,
): ):
""" """
Python generator to traverse the tree (or a subtree) with optional Python generator to traverse the tree (or a subtree) with optional
@ -439,7 +494,7 @@ class Tree(object):
:return: Node IDs that satisfy the conditions :return: Node IDs that satisfy the conditions
:rtype: generator object :rtype: generator object
""" """
nid = self.root if nid is None else nid nid = cast(str, self.root if nid is None else nid)
if not self.contains(nid): if not self.contains(nid):
raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) raise NodeIDAbsentError("Node '%s' is not in the tree" % nid)
@ -470,7 +525,7 @@ class Tree(object):
elif mode is self.ZIGZAG: elif mode is self.ZIGZAG:
# Suggested by Ilya Kuprik (ilya-spy@ynadex.ru). # Suggested by Ilya Kuprik (ilya-spy@ynadex.ru).
stack_fw = [] stack_fw: NodeList = []
queue.reverse() queue.reverse()
stack = stack_bw = queue stack = stack_bw = queue
direction = False direction = False
@ -493,7 +548,7 @@ class Tree(object):
else: else:
raise ValueError("Traversal mode '{}' is not supported".format(mode)) raise ValueError("Traversal mode '{}' is not supported".format(mode))
def filter_nodes(self, func): def filter_nodes(self, func: Callable[[Node], bool]):
""" """
Filters all nodes by function. Filters all nodes by function.
@ -504,7 +559,7 @@ class Tree(object):
""" """
return filter(func, self.all_nodes_itr()) return filter(func, self.all_nodes_itr())
def get_node(self, nid): def get_node(self, nid: Optional[str]) -> Optional[Node]:
""" """
Get the object of the node with ID of ``nid``. Get the object of the node with ID of ``nid``.
@ -531,7 +586,7 @@ class Tree(object):
fpointer = [] fpointer = []
return fpointer return fpointer
def leaves(self, nid=None): def leaves(self, nid: Optional[str] = None) -> NodeList:
"""Get leaves of the whole tree or a subtree.""" """Get leaves of the whole tree or a subtree."""
leaves = [] leaves = []
if nid is None: if nid is None:
@ -555,7 +610,7 @@ class Tree(object):
""" """
return len([n for n in self.rsearch(nid, filter)]) - 1 return len([n for n in self.rsearch(nid, filter)]) - 1
def link_past_node(self, nid): def link_past_node(self, nid: str):
""" """
Delete a node by linking past it. Delete a node by linking past it.
@ -617,7 +672,7 @@ class Tree(object):
"""Return a dict form of nodes in a tree: {id: node_instance}.""" """Return a dict form of nodes in a tree: {id: node_instance}."""
return self._nodes return self._nodes
def parent(self, nid): def parent(self, nid: str) -> Optional[Node]:
"""Get parent :class:`Node` object of given id.""" """Get parent :class:`Node` object of given id."""
if not self.contains(nid): if not self.contains(nid):
raise NodeIDAbsentError("Node '%s' is not in the tree" % nid) raise NodeIDAbsentError("Node '%s' is not in the tree" % nid)
@ -628,7 +683,7 @@ class Tree(object):
return self[pid] return self[pid]
def merge(self, nid, new_tree, deep=False): def merge(self, nid: str, new_tree, deep: bool = False):
"""Patch @new_tree on current tree by pasting new_tree root children on current tree @nid node. """Patch @new_tree on current tree by pasting new_tree root children on current tree @nid node.
Consider the following tree: Consider the following tree:
@ -667,7 +722,7 @@ class Tree(object):
for child in new_tree.children(new_tree.root): for child in new_tree.children(new_tree.root):
self.paste(nid=nid, new_tree=new_tree.subtree(child.identifier), deep=deep) self.paste(nid=nid, new_tree=new_tree.subtree(child.identifier), deep=deep)
def paste(self, nid, new_tree, deep=False): def paste(self, nid: str, new_tree, deep: bool = False):
""" """
Paste a @new_tree to the original one by linking the root Paste a @new_tree to the original one by linking the root
of new tree to given node (nid). of new tree to given node (nid).
@ -698,7 +753,7 @@ class Tree(object):
self.__update_bpointer(new_tree.root, nid) self.__update_bpointer(new_tree.root, nid)
self.__update_fpointer(nid, new_tree.root, self.node_class.ADD) self.__update_fpointer(nid, new_tree.root, self.node_class.ADD)
def paths_to_leaves(self): def paths_to_leaves(self) -> StrLList:
""" """
Use this function to get the identifiers allowing to go from the root Use this function to get the identifiers allowing to go from the root
nodes to each leaf. nodes to each leaf.
@ -735,7 +790,7 @@ class Tree(object):
return res return res
def remove_node(self, identifier): def remove_node(self, identifier: str) -> int:
"""Remove a node indicated by 'identifier' with all its successors. """Remove a node indicated by 'identifier' with all its successors.
Return the number of removed nodes. Return the number of removed nodes.
""" """
@ -762,7 +817,7 @@ class Tree(object):
self.nodes.pop(id_) self.nodes.pop(id_)
return len(removed) return len(removed)
def remove_subtree(self, nid, identifier=None): def remove_subtree(self, nid: str, identifier: Optional[str] = None):
""" """
Get a subtree with ``nid`` being the root. If nid is None, an Get a subtree with ``nid`` being the root. If nid is None, an
empty tree is returned. empty tree is returned.
@ -798,14 +853,16 @@ class Tree(object):
if id_ == self.root: if id_ == self.root:
self.root = None self.root = None
st._nodes.update({id_: self._nodes.pop(id_)}) st._nodes.update({id_: self._nodes.pop(id_)})
st[id_].clone_pointers(self._identifier, st.identifier) st[id_].clone_pointers(
cast(str, self._identifier), cast(str, st.identifier)
)
st[id_].reset_pointers(self._identifier) st[id_].reset_pointers(self._identifier)
if id_ == nid: if id_ == nid:
st[id_].set_predecessor(None, st.identifier) st[id_].set_predecessor(None, st.identifier)
self.__update_fpointer(parent, nid, self.node_class.DELETE) self.__update_fpointer(parent, nid, self.node_class.DELETE)
return st return st
def rsearch(self, nid, filter=None): def rsearch(self, nid: str, filter: Optional[Callable[[Node], bool]] = None):
""" """
Traverse the tree branch along the branch from nid to its Traverse the tree branch along the branch from nid to its
ancestors (until root). ancestors (until root).
@ -833,16 +890,16 @@ class Tree(object):
def save2file( def save2file(
self, self,
filename, filename: str,
nid=None, nid: Optional[str] = None,
level=ROOT, level: int = ROOT,
idhidden=True, idhidden: bool = True,
filter=None, filter: Optional[Callable[[Node], bool]] = None,
key=None, key: Optional[Callable[[Node], Node]] = None,
reverse=False, reverse: bool = False,
line_type="ascii-ex", line_type: str = "ascii-ex",
data_property=None, data_property: Optional[str] = None,
sorting=True, sorting: bool = True,
): ):
""" """
Save the tree into file for offline analysis. Save the tree into file for offline analysis.
@ -869,16 +926,16 @@ class Tree(object):
def show( def show(
self, self,
nid=None, nid: Optional[str] = None,
level=ROOT, level: int = ROOT,
idhidden=True, idhidden: bool = True,
filter=None, filter: Optional[Callable[[Node], bool]] = None,
key=None, key: Optional[Callable[[Node], Node]] = None,
reverse=False, reverse: bool = False,
line_type="ascii-ex", line_type: str = "ascii-ex",
data_property=None, data_property: Optional[str] = None,
stdout=True, stdout: bool = True,
sorting=True, sorting: bool = True,
): ):
""" """
Print the tree structure in hierarchy style. Print the tree structure in hierarchy style.
@ -902,6 +959,7 @@ class Tree(object):
:param reverse: the ``reverse`` param for sorting :class:`Node` objects in the same level. :param reverse: the ``reverse`` param for sorting :class:`Node` objects in the same level.
:param line_type: :param line_type:
:param data_property: the property on the node data object to be printed. :param data_property: the property on the node data object to be printed.
:param stdout: if True print it, if False return printing.
:param sorting: if True perform node sorting, if False return :param sorting: if True perform node sorting, if False return
nodes in original insertion order. In latter case @key and nodes in original insertion order. In latter case @key and
@reverse parameters are ignored. @reverse parameters are ignored.
@ -929,11 +987,11 @@ class Tree(object):
print("Tree is empty") print("Tree is empty")
if stdout: if stdout:
print(self._reader.encode("utf-8")) print(self._reader)
else: else:
return self._reader return self._reader
def siblings(self, nid): def siblings(self, nid: str) -> NodeList:
""" """
Return the siblings of given @nid. Return the siblings of given @nid.
@ -949,7 +1007,7 @@ class Tree(object):
return siblings return siblings
def size(self, level=None): def size(self, level: Optional[int] = None) -> int:
""" """
Get the number of nodes of the whole tree if @level is not Get the number of nodes of the whole tree if @level is not
given. Otherwise, the total number of nodes at specific level given. Otherwise, the total number of nodes at specific level
@ -977,7 +1035,7 @@ class Tree(object):
"level should be an integer instead of '%s'" % type(level) "level should be an integer instead of '%s'" % type(level)
) )
def subtree(self, nid, identifier=None): def subtree(self, nid: str, identifier: Optional[str] = None):
""" """
Return a shallow COPY of subtree with nid being the new root. Return a shallow COPY of subtree with nid being the new root.
If nid is None, return an empty tree. If nid is None, return an empty tree.
@ -1002,13 +1060,15 @@ class Tree(object):
st._nodes.update({self[node_n].identifier: self[node_n]}) st._nodes.update({self[node_n].identifier: self[node_n]})
# define nodes parent/children in this tree # define nodes parent/children in this tree
# all pointers are the same as copied tree, except the root # all pointers are the same as copied tree, except the root
st[node_n].clone_pointers(self._identifier, st.identifier) st[node_n].clone_pointers(
cast(str, self._identifier), cast(str, st.identifier)
)
if node_n == nid: if node_n == nid:
# reset root parent for the new tree # reset root parent for the new tree
st[node_n].set_predecessor(None, st.identifier) st[node_n].set_predecessor(None, st.identifier)
return st return st
def update_node(self, nid, **attrs): def update_node(self, nid: str, **attrs) -> None:
""" """
Update node's attributes. Update node's attributes.
@ -1072,19 +1132,21 @@ class Tree(object):
) )
return tree_dict return tree_dict
def to_json(self, with_data=False, sort=True, reverse=False): def to_json(
self, with_data: bool = False, sort: bool = True, reverse: bool = False
):
"""To format the tree in JSON format.""" """To format the tree in JSON format."""
return json.dumps(self.to_dict(with_data=with_data, sort=sort, reverse=reverse)) return json.dumps(self.to_dict(with_data=with_data, sort=sort, reverse=reverse))
def to_graphviz( def to_graphviz(
self, self,
filename=None, filename: Optional[str] = None,
shape="circle", shape: str = "circle",
graph="digraph", graph: str = "digraph",
filter=None, filter=None,
key=None, key=None,
reverse=False, reverse: bool = False,
sorting=True, sorting: bool = True,
): ):
"""Exports the tree in the dot format of the graphviz software""" """Exports the tree in the dot format of the graphviz software"""
nodes, connections = [], [] nodes, connections = [], []
@ -1097,7 +1159,8 @@ class Tree(object):
sorting=sorting, sorting=sorting,
): ):
nid = self[n].identifier nid = self[n].identifier
state = '"{0}" [label="{1}", shape={2}]'.format(nid, self[n].tag, shape) label = str(self[n].tag).translate(str.maketrans({'"': r"\""}))
state = '"{0}" [label="{1}", shape={2}]'.format(nid, label, shape)
nodes.append(state) nodes.append(state)
for c in self.children(nid): for c in self.children(nid):
@ -1108,7 +1171,7 @@ class Tree(object):
# write nodes and connections to dot format # write nodes and connections to dot format
is_plain_file = filename is not None is_plain_file = filename is not None
if is_plain_file: if is_plain_file:
f = codecs.open(filename, "w", "utf-8") f = codecs.open(cast(str, filename), "w", "utf-8")
else: else:
f = StringIO() f = StringIO()
@ -1119,8 +1182,8 @@ class Tree(object):
if len(connections) > 0: if len(connections) > 0:
f.write("\n") f.write("\n")
for c in connections: for cns in connections:
f.write("\t" + c + "\n") f.write("\t" + cns + "\n")
f.write("}") f.write("}")