Skip to content

Commit a2fd4ec

Browse files
committed
Add strict type hinting
1 parent f74a10e commit a2fd4ec

4 files changed

Lines changed: 70 additions & 57 deletions

File tree

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Run isort
3434
run: isort --check --profile=black .
3535
- name: Run mypy
36-
run: mypy .
36+
run: mypy binarytree
3737
- name: Run pytest
3838
run: py.test --cov=./ --cov-report=xml
3939
- name: Run Sphinx doctest

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ repos:
2424
rev: v0.790
2525
hooks:
2626
- id: mypy
27+
args: [ binarytree ]
2728
- repo: https://gitlab.com/pycqa/flake8
2829
rev: 3.8.4
2930
hooks:

binarytree/__init__.py

Lines changed: 67 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import heapq
44
import random
5+
from dataclasses import dataclass
56
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
67

78
from graphviz import Digraph, nohtml
@@ -25,7 +26,22 @@
2526
VALUE_FIELD = "value"
2627

2728
NodeValue = Union[float, int]
28-
NodeProperty = Union[float, int, bool]
29+
30+
31+
@dataclass
32+
class NodeProperties:
33+
height: int
34+
size: int
35+
is_max_heap: bool
36+
is_min_heap: bool
37+
is_perfect: bool
38+
is_strict: bool
39+
is_complete: bool
40+
leaf_count: int
41+
min_node_value: NodeValue
42+
max_node_value: NodeValue
43+
min_leaf_depth: int
44+
max_leaf_depth: int
2945

3046

3147
class Node:
@@ -39,13 +55,12 @@ class Node:
3955
:param value: Node value (must be a number).
4056
:type value: int | float
4157
:param left: Left child node (default: None).
42-
:type left: binarytree.Node
58+
:type left: binarytree.Node | None
4359
:param right: Right child node (default: None).
44-
:type right: binarytree.Node
60+
:type right: binarytree.Node | None
4561
:raise binarytree.exceptions.NodeTypeError: If left or right child node is
4662
not an instance of :class:`binarytree.Node`.
47-
:raise binarytree.exceptions.NodeValueError: If node value is not a number
48-
(e.g. int, float).
63+
:raise binarytree.exceptions.NodeValueError: If node value is not an int or float.
4964
"""
5065

5166
def __init__(
@@ -180,7 +195,7 @@ def __iter__(self) -> Iterator["Node"]:
180195
https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search
181196
182197
:return: Node iterator.
183-
:rtype: (binarytree.Node)
198+
:rtype: Iterator[binarytree.Node]
184199
185200
**Example**:
186201
@@ -239,7 +254,7 @@ def __len__(self) -> int:
239254
.. note::
240255
This method is equivalent to :attr:`binarytree.Node.size`.
241256
"""
242-
return self.properties["size"]
257+
return sum(1 for _ in iter(self))
243258

244259
def __getitem__(self, index: int) -> "Node":
245260
"""Return the node (or subtree) at the given level-order_ index.
@@ -449,7 +464,7 @@ def _repr_svg_(self) -> str:
449464
.. _Jupyter notebooks: https://jupyter.org
450465
"""
451466
# noinspection PyProtectedMember
452-
return self.graphviz()._repr_svg_()
467+
return str(self.graphviz()._repr_svg_())
453468

454469
def graphviz(self, *args: Any, **kwargs: Any) -> Digraph:
455470
"""Return a graphviz.Digraph_ object representing the binary tree.
@@ -795,7 +810,7 @@ def height(self) -> int:
795810
.. note::
796811
A binary tree with only a root node has a height of 0.
797812
"""
798-
return _get_tree_properties(self)["height"]
813+
return _get_tree_properties(self).height
799814

800815
@property
801816
def size(self) -> int:
@@ -821,7 +836,7 @@ def size(self) -> int:
821836
.. note::
822837
This method is equivalent to :func:`binarytree.Node.__len__`.
823838
"""
824-
return _get_tree_properties(self)["size"]
839+
return self.__len__()
825840

826841
@property
827842
def leaf_count(self) -> int:
@@ -846,7 +861,7 @@ def leaf_count(self) -> int:
846861
>>> root.leaf_count
847862
2
848863
"""
849-
return _get_tree_properties(self)["leaf_count"]
864+
return _get_tree_properties(self).leaf_count
850865

851866
@property
852867
def is_balanced(self) -> bool:
@@ -982,7 +997,7 @@ def is_max_heap(self) -> bool:
982997
>>> root.is_max_heap
983998
True
984999
"""
985-
return _get_tree_properties(self)["is_max_heap"]
1000+
return _get_tree_properties(self).is_max_heap
9861001

9871002
@property
9881003
def is_min_heap(self) -> bool:
@@ -1012,7 +1027,7 @@ def is_min_heap(self) -> bool:
10121027
>>> root.is_min_heap
10131028
True
10141029
"""
1015-
return _get_tree_properties(self)["is_min_heap"]
1030+
return _get_tree_properties(self).is_min_heap
10161031

10171032
@property
10181033
def is_perfect(self) -> bool:
@@ -1049,7 +1064,7 @@ def is_perfect(self) -> bool:
10491064
>>> root.is_perfect
10501065
True
10511066
"""
1052-
return _get_tree_properties(self)["is_perfect"]
1067+
return _get_tree_properties(self).is_perfect
10531068

10541069
@property
10551070
def is_strict(self) -> bool:
@@ -1084,7 +1099,7 @@ def is_strict(self) -> bool:
10841099
>>> root.is_strict
10851100
True
10861101
"""
1087-
return _get_tree_properties(self)["is_strict"]
1102+
return _get_tree_properties(self).is_strict
10881103

10891104
@property
10901105
def is_complete(self) -> bool:
@@ -1121,7 +1136,7 @@ def is_complete(self) -> bool:
11211136
>>> root.is_complete
11221137
True
11231138
"""
1124-
return _get_tree_properties(self)["is_complete"]
1139+
return _get_tree_properties(self).is_complete
11251140

11261141
@property
11271142
def min_node_value(self) -> NodeValue:
@@ -1143,7 +1158,7 @@ def min_node_value(self) -> NodeValue:
11431158
>>> root.min_node_value
11441159
1
11451160
"""
1146-
return _get_tree_properties(self)["min_node_value"]
1161+
return _get_tree_properties(self).min_node_value
11471162

11481163
@property
11491164
def max_node_value(self) -> NodeValue:
@@ -1165,7 +1180,7 @@ def max_node_value(self) -> NodeValue:
11651180
>>> root.max_node_value
11661181
3
11671182
"""
1168-
return _get_tree_properties(self)["max_node_value"]
1183+
return _get_tree_properties(self).max_node_value
11691184

11701185
@property
11711186
def max_leaf_depth(self) -> int:
@@ -1199,7 +1214,7 @@ def max_leaf_depth(self) -> int:
11991214
>>> root.max_leaf_depth
12001215
3
12011216
"""
1202-
return _get_tree_properties(self)["max_leaf_depth"]
1217+
return _get_tree_properties(self).max_leaf_depth
12031218

12041219
@property
12051220
def min_leaf_depth(self) -> int:
@@ -1233,7 +1248,7 @@ def min_leaf_depth(self) -> int:
12331248
>>> root.min_leaf_depth
12341249
1
12351250
"""
1236-
return _get_tree_properties(self)["min_leaf_depth"]
1251+
return _get_tree_properties(self).min_leaf_depth
12371252

12381253
@property
12391254
def properties(self) -> Dict[str, Any]:
@@ -1286,14 +1301,10 @@ def properties(self) -> Dict[str, Any]:
12861301
>>> props['is_strict'] # equivalent to root.is_strict
12871302
True
12881303
"""
1289-
properties = _get_tree_properties(self)
1290-
properties.update(
1291-
{
1292-
"is_bst": _is_bst(self),
1293-
"is_balanced": _is_balanced(self) >= 0,
1294-
"is_symmetric": _is_symmetric(self),
1295-
}
1296-
)
1304+
properties = _get_tree_properties(self).__dict__.copy()
1305+
properties["is_balanced"] = _is_balanced(self) >= 0
1306+
properties["is_bst"] = _is_bst(self)
1307+
properties["is_symmetric"] = _is_symmetric(self)
12971308
return properties
12981309

12991310
@property
@@ -1492,7 +1503,7 @@ def _is_balanced(root: Optional[Node]) -> int:
14921503
"""Return the tree height + 1 if balanced, -1 otherwise.
14931504
14941505
:param root: Root node of the binary tree.
1495-
:type root: binarytree.Node
1506+
:type root: binarytree.Node | None
14961507
:return: Height if the binary tree is balanced, -1 otherwise.
14971508
:rtype: int
14981509
"""
@@ -1511,7 +1522,7 @@ def _is_bst(root: Optional[Node]) -> bool:
15111522
"""Check if the binary tree is a BST (binary search tree).
15121523
15131524
:param root: Root node of the binary tree.
1514-
:type root: binarytree.Node
1525+
:type root: binarytree.Node | None
15151526
:return: True if the binary tree is a BST, False otherwise.
15161527
:rtype: bool
15171528
"""
@@ -1536,26 +1547,26 @@ def _is_symmetric(root: Optional[Node]) -> bool:
15361547
"""Check if the binary tree is symmetric.
15371548
15381549
:param root: Root node of the binary tree.
1539-
:type root: binarytree.Node
1550+
:type root: binarytree.Node | None
15401551
:return: True if the binary tree is symmetric, False otherwise.
15411552
:rtype: bool
15421553
"""
15431554

1544-
def symmetric_helper(left_subtree, right_subtree):
1545-
if left_subtree is None and right_subtree is None:
1555+
def symmetric_helper(left: Optional[Node], right: Optional[Node]) -> bool:
1556+
if left is None and right is None:
15461557
return True
1547-
if left_subtree is None or right_subtree is None:
1558+
if left is None or right is None:
15481559
return False
15491560
return (
1550-
left_subtree.val == right_subtree.val
1551-
and symmetric_helper(left_subtree.left, right_subtree.right)
1552-
and symmetric_helper(left_subtree.right, right_subtree.left)
1561+
left.val == right.val
1562+
and symmetric_helper(left.left, right.right)
1563+
and symmetric_helper(left.right, right.left)
15531564
)
15541565

15551566
return symmetric_helper(root, root)
15561567

15571568

1558-
def _validate_tree_height(height: int):
1569+
def _validate_tree_height(height: int) -> None:
15591570
"""Check if the height of the binary tree is valid.
15601571
15611572
:param height: Height of the binary tree (must be 0 - 9 inclusive).
@@ -1640,7 +1651,7 @@ def _build_tree_string(
16401651
call then combines its left and right sub-boxes to build a larger box etc.
16411652
16421653
:param root: Root node of the binary tree.
1643-
:type root: binarytree.Node
1654+
:type root: binarytree.Node | None
16441655
:param curr_index: Level-order_ index of the current node (root node is 0).
16451656
:type curr_index: int
16461657
:param index: If set to True, include the level-order_ node indexes using
@@ -1717,13 +1728,13 @@ def _build_tree_string(
17171728
return new_box, len(new_box[0]), new_root_start, new_root_end
17181729

17191730

1720-
def _get_tree_properties(root: Node) -> Dict[str, Any]:
1731+
def _get_tree_properties(root: Node) -> NodeProperties:
17211732
"""Inspect the binary tree and return its properties (e.g. height).
17221733
17231734
:param root: Root node of the binary tree.
17241735
:type root: binarytree.Node
17251736
:return: Binary tree properties.
1726-
:rtype: dict
1737+
:rtype: binarytree.NodeProperties
17271738
"""
17281739
is_descending = True
17291740
is_ascending = True
@@ -1779,20 +1790,20 @@ def _get_tree_properties(root: Node) -> Dict[str, Any]:
17791790

17801791
current_level = next_level
17811792

1782-
return {
1783-
"height": max_leaf_depth,
1784-
"size": size,
1785-
"is_max_heap": is_complete and is_descending,
1786-
"is_min_heap": is_complete and is_ascending,
1787-
"is_perfect": leaf_count == 2 ** max_leaf_depth,
1788-
"is_strict": is_strict,
1789-
"is_complete": is_complete,
1790-
"leaf_count": leaf_count,
1791-
"min_node_value": min_node_value,
1792-
"max_node_value": max_node_value,
1793-
"min_leaf_depth": min_leaf_depth,
1794-
"max_leaf_depth": max_leaf_depth,
1795-
}
1793+
return NodeProperties(
1794+
height=max_leaf_depth,
1795+
size=size,
1796+
is_max_heap=is_complete and is_descending,
1797+
is_min_heap=is_complete and is_ascending,
1798+
is_perfect=leaf_count == 2 ** max_leaf_depth,
1799+
is_strict=is_strict,
1800+
is_complete=is_complete,
1801+
leaf_count=leaf_count,
1802+
min_node_value=min_node_value,
1803+
max_node_value=max_node_value,
1804+
min_leaf_depth=min_leaf_depth,
1805+
max_leaf_depth=max_leaf_depth,
1806+
)
17961807

17971808

17981809
def get_parent(root: Node, child: Node) -> Optional[Node]:
@@ -1847,7 +1858,7 @@ def get_parent(root: Node, child: Node) -> Optional[Node]:
18471858
return None
18481859

18491860

1850-
def build(values: List) -> Optional[Node]:
1861+
def build(values: List[int]) -> Optional[Node]:
18511862
"""Build a tree from `list representation`_ and return its root node.
18521863
18531864
.. _list representation:

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ per-file-ignores = __init__.py:F401 conf.py:E402
66

77
[mypy]
88
ignore_missing_imports = True
9+
strict = True

0 commit comments

Comments
 (0)