pytreeds 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytreeds-0.1.0/LICENSE +21 -0
- pytreeds-0.1.0/PKG-INFO +153 -0
- pytreeds-0.1.0/README.md +135 -0
- pytreeds-0.1.0/pyproject.toml +24 -0
- pytreeds-0.1.0/setup.cfg +4 -0
- pytreeds-0.1.0/src/pytreeds/__init__.py +289 -0
- pytreeds-0.1.0/src/pytreeds.egg-info/PKG-INFO +153 -0
- pytreeds-0.1.0/src/pytreeds.egg-info/SOURCES.txt +9 -0
- pytreeds-0.1.0/src/pytreeds.egg-info/dependency_links.txt +1 -0
- pytreeds-0.1.0/src/pytreeds.egg-info/top_level.txt +1 -0
- pytreeds-0.1.0/tests/test_tree.py +101 -0
pytreeds-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pytreeds contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pytreeds-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytreeds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beginner-friendly rooted trees with consistent string navigation and traversal
|
|
5
|
+
Author: pytreeds contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://gitlab.com/ujen.teaching/pytreeds
|
|
8
|
+
Project-URL: Issues, https://gitlab.com/ujen.teaching/pytreeds/-/issues
|
|
9
|
+
Keywords: tree,data structures,education,dfs,bfs,pyodide
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Education
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# pytreeds
|
|
20
|
+
|
|
21
|
+
A dependency-free Python library for beginners to create, inspect, modify, traverse, and print **non-binary trees**. Students keep only one tree variable. Node identifiers are unique strings, and all node-related operations return strings rather than mixed types.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
~~~bash
|
|
26
|
+
pip install pytreeds
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
Pyodide (from your platform's asynchronous package loader):
|
|
30
|
+
|
|
31
|
+
~~~python
|
|
32
|
+
import micropip
|
|
33
|
+
await micropip.install("pytreeds")
|
|
34
|
+
~~~
|
|
35
|
+
|
|
36
|
+
## First example
|
|
37
|
+
|
|
38
|
+
~~~python
|
|
39
|
+
from pytreeds import Tree
|
|
40
|
+
|
|
41
|
+
org = Tree("CEO")
|
|
42
|
+
org.add_nodes(["CTO", "CFO", "COO"], parent="CEO")
|
|
43
|
+
org.add_nodes(["Engineering", "DevOps"], parent="CTO")
|
|
44
|
+
org.add_node("Accounting", parent="CFO")
|
|
45
|
+
|
|
46
|
+
org.show()
|
|
47
|
+
print(org.height()) # 2 edges
|
|
48
|
+
print(org.size()) # 6
|
|
49
|
+
print(org.children("CTO")) # ['Engineering', 'DevOps']
|
|
50
|
+
print(org.parent("DevOps")) # CTO
|
|
51
|
+
print(org.leaves())
|
|
52
|
+
print(list(org.dfs()))
|
|
53
|
+
print(list(org.bfs()))
|
|
54
|
+
|
|
55
|
+
for name in org:
|
|
56
|
+
print(name, "->", org.parent(name))
|
|
57
|
+
~~~
|
|
58
|
+
|
|
59
|
+
Text diagram:
|
|
60
|
+
|
|
61
|
+
~~~text
|
|
62
|
+
CEO
|
|
63
|
+
├── CTO
|
|
64
|
+
│ ├── Engineering
|
|
65
|
+
│ └── DevOps
|
|
66
|
+
├── CFO
|
|
67
|
+
│ └── Accounting
|
|
68
|
+
└── COO
|
|
69
|
+
~~~
|
|
70
|
+
|
|
71
|
+
## API reference
|
|
72
|
+
|
|
73
|
+
### Build and modify
|
|
74
|
+
|
|
75
|
+
| API | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Tree(root=None) | Create an empty tree or one with a root |
|
|
78
|
+
| add_node(name, parent=None) | Create one node |
|
|
79
|
+
| add_nodes(names, parent) | Add several children in insertion order |
|
|
80
|
+
| remove_node(name) | Remove a leaf; raises an error for internal nodes |
|
|
81
|
+
| remove_subtree(name) | Remove a whole branch; removing root empties tree |
|
|
82
|
+
| move_node(name, parent) | Move a whole branch under a different parent |
|
|
83
|
+
| rename_node(old, new) | Rename a node without changing its location |
|
|
84
|
+
| copy() | Return an independent copy |
|
|
85
|
+
|
|
86
|
+
### Properties and relationships
|
|
87
|
+
|
|
88
|
+
| API | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| root | Root name, or None if empty |
|
|
91
|
+
| size() | Total number of nodes |
|
|
92
|
+
| height(name=None) | Height in edges; empty tree = -1 |
|
|
93
|
+
| depth(name) | Number of edges from root; root depth = 0 |
|
|
94
|
+
| leaf_count(name=None) | Number of leaves in tree or subtree |
|
|
95
|
+
| is_leaf(name) | True if the node has no children |
|
|
96
|
+
| contains(name) | True if this name exists |
|
|
97
|
+
| parent(name) | Parent name, or None at root |
|
|
98
|
+
| children(name) | List of child names |
|
|
99
|
+
| siblings(name) | List of sibling names |
|
|
100
|
+
| ancestors(name) | Ancestors from root to parent |
|
|
101
|
+
| descendants(name) | Descendant names in DFS order |
|
|
102
|
+
| leaves(name=None) | Leaf names in DFS order |
|
|
103
|
+
| path(name) | Root-to-node list of names |
|
|
104
|
+
|
|
105
|
+
### Traversal and search
|
|
106
|
+
|
|
107
|
+
| API | Description |
|
|
108
|
+
|---|---|
|
|
109
|
+
| dfs(name=None) | Preorder DFS iterator of strings |
|
|
110
|
+
| bfs(name=None) | Level-order BFS iterator of strings |
|
|
111
|
+
| dfs_postorder(name=None) | Postorder DFS iterator of strings |
|
|
112
|
+
| nodes(name=None) | List of names in preorder |
|
|
113
|
+
| find(text) | Names containing a case-sensitive substring |
|
|
114
|
+
| for name in tree | Default preorder DFS |
|
|
115
|
+
|
|
116
|
+
### Print text
|
|
117
|
+
|
|
118
|
+
| API | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| show(name=None, style="unicode") | Print tree or subtree |
|
|
121
|
+
| to_text(name=None, style="unicode") | Return a diagram string |
|
|
122
|
+
| print(tree) | Print the full diagram |
|
|
123
|
+
|
|
124
|
+
The style option can also be "ascii".
|
|
125
|
+
|
|
126
|
+
## Design and errors
|
|
127
|
+
|
|
128
|
+
- Each node has a unique nonempty string name. No UUIDs, extra node variables, overloaded subscripts, or student-defined classes.
|
|
129
|
+
- Parent, children, leaves, DFS, BFS, and ordinary iteration consistently use strings.
|
|
130
|
+
- The first node is the root. Further additions need a parent.
|
|
131
|
+
- Children and traversals preserve insertion order.
|
|
132
|
+
- Height and depth count edges, not levels.
|
|
133
|
+
- Duplicate names and invalid moves raise ValueError; missing names raise KeyError.
|
|
134
|
+
- Batch insertions validate before modifying the tree.
|
|
135
|
+
- Avoid modifying the tree while looping over a traversal.
|
|
136
|
+
|
|
137
|
+
## Subtree example
|
|
138
|
+
|
|
139
|
+
~~~python
|
|
140
|
+
from pytreeds import Tree
|
|
141
|
+
org = Tree("CEO")
|
|
142
|
+
org.add_nodes(["CTO", "CFO"], parent="CEO")
|
|
143
|
+
org.add_node("DevOps", parent="CTO")
|
|
144
|
+
org.move_node("DevOps", parent="CFO")
|
|
145
|
+
org.rename_node("DevOps", "Infrastructure")
|
|
146
|
+
org.show()
|
|
147
|
+
print(org.path("Infrastructure"))
|
|
148
|
+
org.remove_subtree("CFO")
|
|
149
|
+
~~~
|
|
150
|
+
|
|
151
|
+
Run tests locally using: python -m unittest discover -s tests -v
|
|
152
|
+
|
|
153
|
+
This repository intentionally keeps the existing publishing-only GitLab CI workflow; there is no testing CI job.
|
pytreeds-0.1.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# pytreeds
|
|
2
|
+
|
|
3
|
+
A dependency-free Python library for beginners to create, inspect, modify, traverse, and print **non-binary trees**. Students keep only one tree variable. Node identifiers are unique strings, and all node-related operations return strings rather than mixed types.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
~~~bash
|
|
8
|
+
pip install pytreeds
|
|
9
|
+
~~~
|
|
10
|
+
|
|
11
|
+
Pyodide (from your platform's asynchronous package loader):
|
|
12
|
+
|
|
13
|
+
~~~python
|
|
14
|
+
import micropip
|
|
15
|
+
await micropip.install("pytreeds")
|
|
16
|
+
~~~
|
|
17
|
+
|
|
18
|
+
## First example
|
|
19
|
+
|
|
20
|
+
~~~python
|
|
21
|
+
from pytreeds import Tree
|
|
22
|
+
|
|
23
|
+
org = Tree("CEO")
|
|
24
|
+
org.add_nodes(["CTO", "CFO", "COO"], parent="CEO")
|
|
25
|
+
org.add_nodes(["Engineering", "DevOps"], parent="CTO")
|
|
26
|
+
org.add_node("Accounting", parent="CFO")
|
|
27
|
+
|
|
28
|
+
org.show()
|
|
29
|
+
print(org.height()) # 2 edges
|
|
30
|
+
print(org.size()) # 6
|
|
31
|
+
print(org.children("CTO")) # ['Engineering', 'DevOps']
|
|
32
|
+
print(org.parent("DevOps")) # CTO
|
|
33
|
+
print(org.leaves())
|
|
34
|
+
print(list(org.dfs()))
|
|
35
|
+
print(list(org.bfs()))
|
|
36
|
+
|
|
37
|
+
for name in org:
|
|
38
|
+
print(name, "->", org.parent(name))
|
|
39
|
+
~~~
|
|
40
|
+
|
|
41
|
+
Text diagram:
|
|
42
|
+
|
|
43
|
+
~~~text
|
|
44
|
+
CEO
|
|
45
|
+
├── CTO
|
|
46
|
+
│ ├── Engineering
|
|
47
|
+
│ └── DevOps
|
|
48
|
+
├── CFO
|
|
49
|
+
│ └── Accounting
|
|
50
|
+
└── COO
|
|
51
|
+
~~~
|
|
52
|
+
|
|
53
|
+
## API reference
|
|
54
|
+
|
|
55
|
+
### Build and modify
|
|
56
|
+
|
|
57
|
+
| API | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Tree(root=None) | Create an empty tree or one with a root |
|
|
60
|
+
| add_node(name, parent=None) | Create one node |
|
|
61
|
+
| add_nodes(names, parent) | Add several children in insertion order |
|
|
62
|
+
| remove_node(name) | Remove a leaf; raises an error for internal nodes |
|
|
63
|
+
| remove_subtree(name) | Remove a whole branch; removing root empties tree |
|
|
64
|
+
| move_node(name, parent) | Move a whole branch under a different parent |
|
|
65
|
+
| rename_node(old, new) | Rename a node without changing its location |
|
|
66
|
+
| copy() | Return an independent copy |
|
|
67
|
+
|
|
68
|
+
### Properties and relationships
|
|
69
|
+
|
|
70
|
+
| API | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| root | Root name, or None if empty |
|
|
73
|
+
| size() | Total number of nodes |
|
|
74
|
+
| height(name=None) | Height in edges; empty tree = -1 |
|
|
75
|
+
| depth(name) | Number of edges from root; root depth = 0 |
|
|
76
|
+
| leaf_count(name=None) | Number of leaves in tree or subtree |
|
|
77
|
+
| is_leaf(name) | True if the node has no children |
|
|
78
|
+
| contains(name) | True if this name exists |
|
|
79
|
+
| parent(name) | Parent name, or None at root |
|
|
80
|
+
| children(name) | List of child names |
|
|
81
|
+
| siblings(name) | List of sibling names |
|
|
82
|
+
| ancestors(name) | Ancestors from root to parent |
|
|
83
|
+
| descendants(name) | Descendant names in DFS order |
|
|
84
|
+
| leaves(name=None) | Leaf names in DFS order |
|
|
85
|
+
| path(name) | Root-to-node list of names |
|
|
86
|
+
|
|
87
|
+
### Traversal and search
|
|
88
|
+
|
|
89
|
+
| API | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| dfs(name=None) | Preorder DFS iterator of strings |
|
|
92
|
+
| bfs(name=None) | Level-order BFS iterator of strings |
|
|
93
|
+
| dfs_postorder(name=None) | Postorder DFS iterator of strings |
|
|
94
|
+
| nodes(name=None) | List of names in preorder |
|
|
95
|
+
| find(text) | Names containing a case-sensitive substring |
|
|
96
|
+
| for name in tree | Default preorder DFS |
|
|
97
|
+
|
|
98
|
+
### Print text
|
|
99
|
+
|
|
100
|
+
| API | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| show(name=None, style="unicode") | Print tree or subtree |
|
|
103
|
+
| to_text(name=None, style="unicode") | Return a diagram string |
|
|
104
|
+
| print(tree) | Print the full diagram |
|
|
105
|
+
|
|
106
|
+
The style option can also be "ascii".
|
|
107
|
+
|
|
108
|
+
## Design and errors
|
|
109
|
+
|
|
110
|
+
- Each node has a unique nonempty string name. No UUIDs, extra node variables, overloaded subscripts, or student-defined classes.
|
|
111
|
+
- Parent, children, leaves, DFS, BFS, and ordinary iteration consistently use strings.
|
|
112
|
+
- The first node is the root. Further additions need a parent.
|
|
113
|
+
- Children and traversals preserve insertion order.
|
|
114
|
+
- Height and depth count edges, not levels.
|
|
115
|
+
- Duplicate names and invalid moves raise ValueError; missing names raise KeyError.
|
|
116
|
+
- Batch insertions validate before modifying the tree.
|
|
117
|
+
- Avoid modifying the tree while looping over a traversal.
|
|
118
|
+
|
|
119
|
+
## Subtree example
|
|
120
|
+
|
|
121
|
+
~~~python
|
|
122
|
+
from pytreeds import Tree
|
|
123
|
+
org = Tree("CEO")
|
|
124
|
+
org.add_nodes(["CTO", "CFO"], parent="CEO")
|
|
125
|
+
org.add_node("DevOps", parent="CTO")
|
|
126
|
+
org.move_node("DevOps", parent="CFO")
|
|
127
|
+
org.rename_node("DevOps", "Infrastructure")
|
|
128
|
+
org.show()
|
|
129
|
+
print(org.path("Infrastructure"))
|
|
130
|
+
org.remove_subtree("CFO")
|
|
131
|
+
~~~
|
|
132
|
+
|
|
133
|
+
Run tests locally using: python -m unittest discover -s tests -v
|
|
134
|
+
|
|
135
|
+
This repository intentionally keeps the existing publishing-only GitLab CI workflow; there is no testing CI job.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytreeds"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Beginner-friendly rooted trees with consistent string navigation and traversal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "pytreeds contributors"}]
|
|
13
|
+
keywords = ["tree", "data structures", "education", "dfs", "bfs", "pyodide"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Education",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Education",
|
|
19
|
+
]
|
|
20
|
+
[project.urls]
|
|
21
|
+
Source = "https://gitlab.com/ujen.teaching/pytreeds"
|
|
22
|
+
Issues = "https://gitlab.com/ujen.teaching/pytreeds/-/issues"
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
pytreeds-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Simple, dependency-free rooted trees for Python beginners."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from collections import deque
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = ["Tree"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Tree:
|
|
12
|
+
"""An ordered tree of uniquely named nodes; all public node values are strings."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, root: str | None = None):
|
|
15
|
+
self._root: str | None = None
|
|
16
|
+
self._children: dict[str, list[str]] = {}
|
|
17
|
+
self._parents: dict[str, str | None] = {}
|
|
18
|
+
if root is not None:
|
|
19
|
+
self.add_node(root)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _check_name(name: str) -> None:
|
|
23
|
+
if not isinstance(name, str) or not name.strip():
|
|
24
|
+
raise ValueError("Node names must be non-empty strings")
|
|
25
|
+
|
|
26
|
+
def _require(self, name: str) -> None:
|
|
27
|
+
if name not in self._parents:
|
|
28
|
+
raise KeyError(f"Node not found: {name!r}")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def root(self) -> str | None:
|
|
32
|
+
"""Return the root name, or None if the tree is empty."""
|
|
33
|
+
return self._root
|
|
34
|
+
|
|
35
|
+
def add_node(self, name: str, parent: str | None = None) -> None:
|
|
36
|
+
"""Add the root to an empty tree or add a child under parent."""
|
|
37
|
+
self._check_name(name)
|
|
38
|
+
if name in self._parents:
|
|
39
|
+
raise ValueError(f"Node already exists: {name!r}")
|
|
40
|
+
if self._root is None:
|
|
41
|
+
if parent is not None:
|
|
42
|
+
raise ValueError("The first node cannot have a parent")
|
|
43
|
+
self._root = name
|
|
44
|
+
else:
|
|
45
|
+
if parent is None:
|
|
46
|
+
raise ValueError("A parent is required for new non-root nodes")
|
|
47
|
+
self._require(parent)
|
|
48
|
+
self._children[parent].append(name)
|
|
49
|
+
self._children[name] = []
|
|
50
|
+
self._parents[name] = parent
|
|
51
|
+
|
|
52
|
+
def add_nodes(self, names: Iterable[str], parent: str) -> None:
|
|
53
|
+
"""Add several children in order, after validating all names."""
|
|
54
|
+
self._require(parent)
|
|
55
|
+
if isinstance(names, (str, bytes)):
|
|
56
|
+
raise TypeError("Pass a list of node names, not a single string")
|
|
57
|
+
names = list(names)
|
|
58
|
+
if any(not isinstance(name, str) or not name.strip() for name in names):
|
|
59
|
+
raise ValueError("Node names must be non-empty strings")
|
|
60
|
+
if len(names) != len(set(names)) or any(name in self._parents for name in names):
|
|
61
|
+
raise ValueError("Node names must be unique within the tree")
|
|
62
|
+
for name in names:
|
|
63
|
+
self.add_node(name, parent=parent)
|
|
64
|
+
|
|
65
|
+
def contains(self, name: str) -> bool:
|
|
66
|
+
"""Test whether a node exists."""
|
|
67
|
+
return isinstance(name, str) and name in self._parents
|
|
68
|
+
|
|
69
|
+
def __contains__(self, name: str) -> bool:
|
|
70
|
+
return self.contains(name)
|
|
71
|
+
|
|
72
|
+
def size(self) -> int:
|
|
73
|
+
return len(self._parents)
|
|
74
|
+
|
|
75
|
+
def depth(self, name: str) -> int:
|
|
76
|
+
"""Count edges from root to node, starting at zero."""
|
|
77
|
+
self._require(name)
|
|
78
|
+
result = 0
|
|
79
|
+
while self._parents[name] is not None:
|
|
80
|
+
name = self._parents[name]
|
|
81
|
+
result += 1
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def height(self, name: str | None = None) -> int:
|
|
85
|
+
"""Maximum downward edge count; -1 for an empty tree."""
|
|
86
|
+
if name is None and self._root is None:
|
|
87
|
+
return -1
|
|
88
|
+
name = self._start(name)
|
|
89
|
+
longest = 0
|
|
90
|
+
queue = deque([(name, 0)])
|
|
91
|
+
while queue:
|
|
92
|
+
current, level = queue.popleft()
|
|
93
|
+
longest = max(longest, level)
|
|
94
|
+
queue.extend((child, level + 1) for child in self._children[current])
|
|
95
|
+
return longest
|
|
96
|
+
|
|
97
|
+
def is_leaf(self, name: str) -> bool:
|
|
98
|
+
self._require(name)
|
|
99
|
+
return not self._children[name]
|
|
100
|
+
|
|
101
|
+
def parent(self, name: str) -> str | None:
|
|
102
|
+
self._require(name)
|
|
103
|
+
return self._parents[name]
|
|
104
|
+
|
|
105
|
+
def children(self, name: str) -> list[str]:
|
|
106
|
+
self._require(name)
|
|
107
|
+
return self._children[name].copy()
|
|
108
|
+
|
|
109
|
+
def siblings(self, name: str) -> list[str]:
|
|
110
|
+
self._require(name)
|
|
111
|
+
parent = self._parents[name]
|
|
112
|
+
return [] if parent is None else [child for child in self._children[parent] if child != name]
|
|
113
|
+
|
|
114
|
+
def ancestors(self, name: str) -> list[str]:
|
|
115
|
+
"""Return ancestors from root to parent, excluding the node."""
|
|
116
|
+
self._require(name)
|
|
117
|
+
result = []
|
|
118
|
+
parent = self._parents[name]
|
|
119
|
+
while parent is not None:
|
|
120
|
+
result.append(parent)
|
|
121
|
+
parent = self._parents[parent]
|
|
122
|
+
return result[::-1]
|
|
123
|
+
|
|
124
|
+
def path(self, name: str) -> list[str]:
|
|
125
|
+
return self.ancestors(name) + [name]
|
|
126
|
+
|
|
127
|
+
def _start(self, name: str | None) -> str | None:
|
|
128
|
+
if name is not None:
|
|
129
|
+
self._require(name)
|
|
130
|
+
return name
|
|
131
|
+
return self._root
|
|
132
|
+
|
|
133
|
+
def dfs(self, name: str | None = None) -> Iterator[str]:
|
|
134
|
+
"""Yield names using preorder depth-first traversal."""
|
|
135
|
+
start = self._start(name)
|
|
136
|
+
if start is None:
|
|
137
|
+
return
|
|
138
|
+
stack = [start]
|
|
139
|
+
while stack:
|
|
140
|
+
current = stack.pop()
|
|
141
|
+
yield current
|
|
142
|
+
stack.extend(reversed(self._children[current]))
|
|
143
|
+
|
|
144
|
+
def bfs(self, name: str | None = None) -> Iterator[str]:
|
|
145
|
+
"""Yield names in breadth-first (level) order."""
|
|
146
|
+
start = self._start(name)
|
|
147
|
+
if start is None:
|
|
148
|
+
return
|
|
149
|
+
queue = deque([start])
|
|
150
|
+
while queue:
|
|
151
|
+
current = queue.popleft()
|
|
152
|
+
yield current
|
|
153
|
+
queue.extend(self._children[current])
|
|
154
|
+
|
|
155
|
+
def dfs_postorder(self, name: str | None = None) -> Iterator[str]:
|
|
156
|
+
"""Yield names using depth-first postorder traversal."""
|
|
157
|
+
start = self._start(name)
|
|
158
|
+
if start is None:
|
|
159
|
+
return
|
|
160
|
+
stack = [(start, False)]
|
|
161
|
+
while stack:
|
|
162
|
+
current, ready = stack.pop()
|
|
163
|
+
if ready:
|
|
164
|
+
yield current
|
|
165
|
+
else:
|
|
166
|
+
stack.append((current, True))
|
|
167
|
+
stack.extend((child, False) for child in reversed(self._children[current]))
|
|
168
|
+
|
|
169
|
+
def __iter__(self) -> Iterator[str]:
|
|
170
|
+
return self.dfs()
|
|
171
|
+
|
|
172
|
+
def nodes(self, name: str | None = None) -> list[str]:
|
|
173
|
+
return list(self.dfs(name))
|
|
174
|
+
|
|
175
|
+
def descendants(self, name: str) -> list[str]:
|
|
176
|
+
self._require(name)
|
|
177
|
+
iterator = self.dfs(name)
|
|
178
|
+
next(iterator)
|
|
179
|
+
return list(iterator)
|
|
180
|
+
|
|
181
|
+
def leaves(self, name: str | None = None) -> list[str]:
|
|
182
|
+
return [node for node in self.dfs(name) if not self._children[node]]
|
|
183
|
+
|
|
184
|
+
def leaf_count(self, name: str | None = None) -> int:
|
|
185
|
+
return len(self.leaves(name))
|
|
186
|
+
|
|
187
|
+
def find(self, text: str) -> list[str]:
|
|
188
|
+
"""Find names containing the given case-sensitive substring."""
|
|
189
|
+
if not isinstance(text, str):
|
|
190
|
+
raise TypeError("Search text must be a string")
|
|
191
|
+
return [name for name in self if text in name]
|
|
192
|
+
|
|
193
|
+
def remove_node(self, name: str) -> None:
|
|
194
|
+
"""Remove a leaf; use remove_subtree() to remove a branch."""
|
|
195
|
+
self._require(name)
|
|
196
|
+
if self._children[name]:
|
|
197
|
+
raise ValueError("Node has children; use remove_subtree()")
|
|
198
|
+
parent = self._parents[name]
|
|
199
|
+
if parent is None:
|
|
200
|
+
self._root = None
|
|
201
|
+
else:
|
|
202
|
+
self._children[parent].remove(name)
|
|
203
|
+
del self._parents[name]
|
|
204
|
+
del self._children[name]
|
|
205
|
+
|
|
206
|
+
def remove_subtree(self, name: str) -> None:
|
|
207
|
+
"""Remove a node and all descendants. Removing root empties tree."""
|
|
208
|
+
self._require(name)
|
|
209
|
+
parent = self._parents[name]
|
|
210
|
+
if parent is None:
|
|
211
|
+
self._root = None
|
|
212
|
+
else:
|
|
213
|
+
self._children[parent].remove(name)
|
|
214
|
+
for node in list(self.dfs(name)):
|
|
215
|
+
del self._parents[node]
|
|
216
|
+
del self._children[node]
|
|
217
|
+
|
|
218
|
+
def move_node(self, name: str, parent: str) -> None:
|
|
219
|
+
"""Move a whole subtree, appending it to the new parent's children."""
|
|
220
|
+
self._require(name)
|
|
221
|
+
self._require(parent)
|
|
222
|
+
if name == self._root:
|
|
223
|
+
raise ValueError("Cannot move the root")
|
|
224
|
+
if parent == name or parent in self.descendants(name):
|
|
225
|
+
raise ValueError("Cannot move a node below itself or its descendants")
|
|
226
|
+
old_parent = self._parents[name]
|
|
227
|
+
self._children[old_parent].remove(name)
|
|
228
|
+
self._children[parent].append(name)
|
|
229
|
+
self._parents[name] = parent
|
|
230
|
+
|
|
231
|
+
def rename_node(self, old: str, new: str) -> None:
|
|
232
|
+
"""Change a node name without changing its location."""
|
|
233
|
+
self._require(old)
|
|
234
|
+
self._check_name(new)
|
|
235
|
+
if new in self._parents:
|
|
236
|
+
raise ValueError(f"Node already exists: {new!r}")
|
|
237
|
+
parent = self._parents.pop(old)
|
|
238
|
+
children = self._children.pop(old)
|
|
239
|
+
self._parents[new] = parent
|
|
240
|
+
self._children[new] = children
|
|
241
|
+
if parent is None:
|
|
242
|
+
self._root = new
|
|
243
|
+
else:
|
|
244
|
+
siblings = self._children[parent]
|
|
245
|
+
siblings[siblings.index(old)] = new
|
|
246
|
+
for child in children:
|
|
247
|
+
self._parents[child] = new
|
|
248
|
+
|
|
249
|
+
def copy(self) -> Tree:
|
|
250
|
+
"""Return an independent copy."""
|
|
251
|
+
other = Tree()
|
|
252
|
+
other._root = self._root
|
|
253
|
+
other._parents = self._parents.copy()
|
|
254
|
+
other._children = {name: children.copy() for name, children in self._children.items()}
|
|
255
|
+
return other
|
|
256
|
+
|
|
257
|
+
def to_text(self, name: str | None = None, style: str = "unicode") -> str:
|
|
258
|
+
"""Return an indented Unicode or ASCII branch diagram."""
|
|
259
|
+
if style not in ("unicode", "ascii"):
|
|
260
|
+
raise ValueError("style must be 'unicode' or 'ascii'")
|
|
261
|
+
start = self._start(name)
|
|
262
|
+
if start is None:
|
|
263
|
+
return "(empty tree)"
|
|
264
|
+
if style == "unicode":
|
|
265
|
+
branch, last_branch, middle, blank = "├── ", "└── ", "│ ", " "
|
|
266
|
+
else:
|
|
267
|
+
branch, last_branch, middle, blank = "|-- ", chr(96) + "-- ", "| ", " "
|
|
268
|
+
lines = [start]
|
|
269
|
+
stack = []
|
|
270
|
+
children = self._children[start]
|
|
271
|
+
for i in range(len(children) - 1, -1, -1):
|
|
272
|
+
stack.append((children[i], "", i == len(children) - 1))
|
|
273
|
+
while stack:
|
|
274
|
+
current, prefix, is_last = stack.pop()
|
|
275
|
+
lines.append(prefix + (last_branch if is_last else branch) + current)
|
|
276
|
+
next_prefix = prefix + (blank if is_last else middle)
|
|
277
|
+
children = self._children[current]
|
|
278
|
+
for i in range(len(children) - 1, -1, -1):
|
|
279
|
+
stack.append((children[i], next_prefix, i == len(children) - 1))
|
|
280
|
+
return "\n".join(lines)
|
|
281
|
+
|
|
282
|
+
def show(self, name: str | None = None, style: str = "unicode") -> None:
|
|
283
|
+
print(self.to_text(name, style=style))
|
|
284
|
+
|
|
285
|
+
def __str__(self) -> str:
|
|
286
|
+
return self.to_text()
|
|
287
|
+
|
|
288
|
+
def __repr__(self) -> str:
|
|
289
|
+
return f"Tree(root={self._root!r}, size={self.size()})"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytreeds
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beginner-friendly rooted trees with consistent string navigation and traversal
|
|
5
|
+
Author: pytreeds contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://gitlab.com/ujen.teaching/pytreeds
|
|
8
|
+
Project-URL: Issues, https://gitlab.com/ujen.teaching/pytreeds/-/issues
|
|
9
|
+
Keywords: tree,data structures,education,dfs,bfs,pyodide
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Education
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# pytreeds
|
|
20
|
+
|
|
21
|
+
A dependency-free Python library for beginners to create, inspect, modify, traverse, and print **non-binary trees**. Students keep only one tree variable. Node identifiers are unique strings, and all node-related operations return strings rather than mixed types.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
~~~bash
|
|
26
|
+
pip install pytreeds
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
Pyodide (from your platform's asynchronous package loader):
|
|
30
|
+
|
|
31
|
+
~~~python
|
|
32
|
+
import micropip
|
|
33
|
+
await micropip.install("pytreeds")
|
|
34
|
+
~~~
|
|
35
|
+
|
|
36
|
+
## First example
|
|
37
|
+
|
|
38
|
+
~~~python
|
|
39
|
+
from pytreeds import Tree
|
|
40
|
+
|
|
41
|
+
org = Tree("CEO")
|
|
42
|
+
org.add_nodes(["CTO", "CFO", "COO"], parent="CEO")
|
|
43
|
+
org.add_nodes(["Engineering", "DevOps"], parent="CTO")
|
|
44
|
+
org.add_node("Accounting", parent="CFO")
|
|
45
|
+
|
|
46
|
+
org.show()
|
|
47
|
+
print(org.height()) # 2 edges
|
|
48
|
+
print(org.size()) # 6
|
|
49
|
+
print(org.children("CTO")) # ['Engineering', 'DevOps']
|
|
50
|
+
print(org.parent("DevOps")) # CTO
|
|
51
|
+
print(org.leaves())
|
|
52
|
+
print(list(org.dfs()))
|
|
53
|
+
print(list(org.bfs()))
|
|
54
|
+
|
|
55
|
+
for name in org:
|
|
56
|
+
print(name, "->", org.parent(name))
|
|
57
|
+
~~~
|
|
58
|
+
|
|
59
|
+
Text diagram:
|
|
60
|
+
|
|
61
|
+
~~~text
|
|
62
|
+
CEO
|
|
63
|
+
├── CTO
|
|
64
|
+
│ ├── Engineering
|
|
65
|
+
│ └── DevOps
|
|
66
|
+
├── CFO
|
|
67
|
+
│ └── Accounting
|
|
68
|
+
└── COO
|
|
69
|
+
~~~
|
|
70
|
+
|
|
71
|
+
## API reference
|
|
72
|
+
|
|
73
|
+
### Build and modify
|
|
74
|
+
|
|
75
|
+
| API | Description |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Tree(root=None) | Create an empty tree or one with a root |
|
|
78
|
+
| add_node(name, parent=None) | Create one node |
|
|
79
|
+
| add_nodes(names, parent) | Add several children in insertion order |
|
|
80
|
+
| remove_node(name) | Remove a leaf; raises an error for internal nodes |
|
|
81
|
+
| remove_subtree(name) | Remove a whole branch; removing root empties tree |
|
|
82
|
+
| move_node(name, parent) | Move a whole branch under a different parent |
|
|
83
|
+
| rename_node(old, new) | Rename a node without changing its location |
|
|
84
|
+
| copy() | Return an independent copy |
|
|
85
|
+
|
|
86
|
+
### Properties and relationships
|
|
87
|
+
|
|
88
|
+
| API | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| root | Root name, or None if empty |
|
|
91
|
+
| size() | Total number of nodes |
|
|
92
|
+
| height(name=None) | Height in edges; empty tree = -1 |
|
|
93
|
+
| depth(name) | Number of edges from root; root depth = 0 |
|
|
94
|
+
| leaf_count(name=None) | Number of leaves in tree or subtree |
|
|
95
|
+
| is_leaf(name) | True if the node has no children |
|
|
96
|
+
| contains(name) | True if this name exists |
|
|
97
|
+
| parent(name) | Parent name, or None at root |
|
|
98
|
+
| children(name) | List of child names |
|
|
99
|
+
| siblings(name) | List of sibling names |
|
|
100
|
+
| ancestors(name) | Ancestors from root to parent |
|
|
101
|
+
| descendants(name) | Descendant names in DFS order |
|
|
102
|
+
| leaves(name=None) | Leaf names in DFS order |
|
|
103
|
+
| path(name) | Root-to-node list of names |
|
|
104
|
+
|
|
105
|
+
### Traversal and search
|
|
106
|
+
|
|
107
|
+
| API | Description |
|
|
108
|
+
|---|---|
|
|
109
|
+
| dfs(name=None) | Preorder DFS iterator of strings |
|
|
110
|
+
| bfs(name=None) | Level-order BFS iterator of strings |
|
|
111
|
+
| dfs_postorder(name=None) | Postorder DFS iterator of strings |
|
|
112
|
+
| nodes(name=None) | List of names in preorder |
|
|
113
|
+
| find(text) | Names containing a case-sensitive substring |
|
|
114
|
+
| for name in tree | Default preorder DFS |
|
|
115
|
+
|
|
116
|
+
### Print text
|
|
117
|
+
|
|
118
|
+
| API | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| show(name=None, style="unicode") | Print tree or subtree |
|
|
121
|
+
| to_text(name=None, style="unicode") | Return a diagram string |
|
|
122
|
+
| print(tree) | Print the full diagram |
|
|
123
|
+
|
|
124
|
+
The style option can also be "ascii".
|
|
125
|
+
|
|
126
|
+
## Design and errors
|
|
127
|
+
|
|
128
|
+
- Each node has a unique nonempty string name. No UUIDs, extra node variables, overloaded subscripts, or student-defined classes.
|
|
129
|
+
- Parent, children, leaves, DFS, BFS, and ordinary iteration consistently use strings.
|
|
130
|
+
- The first node is the root. Further additions need a parent.
|
|
131
|
+
- Children and traversals preserve insertion order.
|
|
132
|
+
- Height and depth count edges, not levels.
|
|
133
|
+
- Duplicate names and invalid moves raise ValueError; missing names raise KeyError.
|
|
134
|
+
- Batch insertions validate before modifying the tree.
|
|
135
|
+
- Avoid modifying the tree while looping over a traversal.
|
|
136
|
+
|
|
137
|
+
## Subtree example
|
|
138
|
+
|
|
139
|
+
~~~python
|
|
140
|
+
from pytreeds import Tree
|
|
141
|
+
org = Tree("CEO")
|
|
142
|
+
org.add_nodes(["CTO", "CFO"], parent="CEO")
|
|
143
|
+
org.add_node("DevOps", parent="CTO")
|
|
144
|
+
org.move_node("DevOps", parent="CFO")
|
|
145
|
+
org.rename_node("DevOps", "Infrastructure")
|
|
146
|
+
org.show()
|
|
147
|
+
print(org.path("Infrastructure"))
|
|
148
|
+
org.remove_subtree("CFO")
|
|
149
|
+
~~~
|
|
150
|
+
|
|
151
|
+
Run tests locally using: python -m unittest discover -s tests -v
|
|
152
|
+
|
|
153
|
+
This repository intentionally keeps the existing publishing-only GitLab CI workflow; there is no testing CI job.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytreeds
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import unittest
|
|
3
|
+
from contextlib import redirect_stdout
|
|
4
|
+
from pytreeds import Tree
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTree(unittest.TestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.t = Tree("A")
|
|
10
|
+
self.t.add_nodes(["B", "C", "D"], parent="A")
|
|
11
|
+
self.t.add_nodes(["E", "F", "G"], parent="B")
|
|
12
|
+
self.t.add_nodes(["H", "I"], parent="C")
|
|
13
|
+
|
|
14
|
+
def test_properties(self):
|
|
15
|
+
t = self.t
|
|
16
|
+
self.assertEqual(t.size(), 9)
|
|
17
|
+
self.assertEqual(t.height(), 2)
|
|
18
|
+
self.assertEqual(t.height("B"), 1)
|
|
19
|
+
self.assertEqual(t.depth("G"), 2)
|
|
20
|
+
self.assertEqual(t.parent("G"), "B")
|
|
21
|
+
self.assertEqual(t.children("B"), ["E", "F", "G"])
|
|
22
|
+
self.assertEqual(t.siblings("B"), ["C", "D"])
|
|
23
|
+
self.assertEqual(t.ancestors("G"), ["A", "B"])
|
|
24
|
+
self.assertEqual(t.path("G"), ["A", "B", "G"])
|
|
25
|
+
self.assertEqual(t.descendants("B"), ["E", "F", "G"])
|
|
26
|
+
self.assertEqual(t.leaves(), ["E", "F", "G", "H", "I", "D"])
|
|
27
|
+
self.assertEqual(t.leaf_count(), 6)
|
|
28
|
+
self.assertTrue(t.is_leaf("G"))
|
|
29
|
+
self.assertFalse(t.is_leaf("B"))
|
|
30
|
+
|
|
31
|
+
def test_traversal(self):
|
|
32
|
+
self.assertEqual(list(self.t.dfs()), ["A", "B", "E", "F", "G", "C", "H", "I", "D"])
|
|
33
|
+
self.assertEqual(list(self.t.bfs()), ["A", "B", "C", "D", "E", "F", "G", "H", "I"])
|
|
34
|
+
self.assertEqual(list(self.t.dfs_postorder()), ["E", "F", "G", "B", "H", "I", "C", "D", "A"])
|
|
35
|
+
self.assertEqual(list(self.t), list(self.t.dfs()))
|
|
36
|
+
self.assertEqual(list(self.t.bfs("C")), ["C", "H", "I"])
|
|
37
|
+
self.assertEqual(self.t.nodes("B"), ["B", "E", "F", "G"])
|
|
38
|
+
self.assertEqual(self.t.find("G"), ["G"])
|
|
39
|
+
|
|
40
|
+
def test_changes(self):
|
|
41
|
+
t = self.t
|
|
42
|
+
with self.assertRaises(ValueError):
|
|
43
|
+
t.move_node("B", "F")
|
|
44
|
+
with self.assertRaises(ValueError):
|
|
45
|
+
t.remove_node("B")
|
|
46
|
+
t.move_node("F", "C")
|
|
47
|
+
t.rename_node("C", "X")
|
|
48
|
+
self.assertEqual(t.path("F"), ["A", "X", "F"])
|
|
49
|
+
t.remove_node("I")
|
|
50
|
+
t.remove_subtree("B")
|
|
51
|
+
self.assertFalse(t.contains("B"))
|
|
52
|
+
self.assertFalse(t.contains("E"))
|
|
53
|
+
self.assertTrue(t.contains("X"))
|
|
54
|
+
|
|
55
|
+
def test_invalid_and_copy(self):
|
|
56
|
+
with self.assertRaises(ValueError):
|
|
57
|
+
self.t.add_node("A", parent="B")
|
|
58
|
+
with self.assertRaises(KeyError):
|
|
59
|
+
self.t.add_node("Z", parent="missing")
|
|
60
|
+
with self.assertRaises(ValueError):
|
|
61
|
+
self.t.add_nodes(["new", "C"], parent="B")
|
|
62
|
+
self.assertFalse(self.t.contains("new"))
|
|
63
|
+
with self.assertRaises(TypeError):
|
|
64
|
+
self.t.add_nodes("xyz", parent="B")
|
|
65
|
+
copied = self.t.copy()
|
|
66
|
+
copied.rename_node("A", "ROOT")
|
|
67
|
+
self.assertEqual(self.t.root, "A")
|
|
68
|
+
self.assertEqual(copied.root, "ROOT")
|
|
69
|
+
|
|
70
|
+
def test_text(self):
|
|
71
|
+
expected = "A\n├── B\n│ ├── E\n│ ├── F\n│ └── G\n├── C\n│ ├── H\n│ └── I\n└── D"
|
|
72
|
+
self.assertEqual(self.t.to_text(), expected)
|
|
73
|
+
self.assertEqual(self.t.to_text("B"), "B\n├── E\n├── F\n└── G")
|
|
74
|
+
self.assertIn(chr(96) + "-- D", self.t.to_text(style="ascii"))
|
|
75
|
+
with io.StringIO() as output, redirect_stdout(output):
|
|
76
|
+
self.t.show("C")
|
|
77
|
+
self.assertEqual(output.getvalue(), "C\n├── H\n└── I\n")
|
|
78
|
+
|
|
79
|
+
def test_empty(self):
|
|
80
|
+
t = Tree()
|
|
81
|
+
self.assertEqual(t.height(), -1)
|
|
82
|
+
self.assertEqual(list(t.dfs()), [])
|
|
83
|
+
self.assertEqual(list(t.bfs()), [])
|
|
84
|
+
self.assertEqual(list(t.dfs_postorder()), [])
|
|
85
|
+
t.add_node("A")
|
|
86
|
+
t.remove_node("A")
|
|
87
|
+
self.assertIsNone(t.root)
|
|
88
|
+
self.t.remove_subtree("A")
|
|
89
|
+
self.assertEqual(self.t.size(), 0)
|
|
90
|
+
|
|
91
|
+
def test_deep(self):
|
|
92
|
+
t = Tree("0")
|
|
93
|
+
for n in range(1, 1200):
|
|
94
|
+
t.add_node(str(n), parent=str(n - 1))
|
|
95
|
+
self.assertEqual(t.height(), 1199)
|
|
96
|
+
self.assertEqual(len(t.to_text().splitlines()), 1200)
|
|
97
|
+
self.assertEqual(len(list(t.dfs_postorder())), 1200)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
unittest.main()
|