From fddb6bbd8c9826f621d92cc860f73cb675b4e162 Mon Sep 17 00:00:00 2001 From: Joohwan Oh Date: Wed, 23 Mar 2022 03:55:45 -0700 Subject: [PATCH] Add support for string node values --- README.md | 6 +- binarytree/__init__.py | 157 ++++++---- binarytree/exceptions.py | 2 +- docs/overview.rst | 4 +- tests/test_tree.py | 617 ++++++++++++++++++++++++++++++++------- tests/utils.py | 21 +- 6 files changed, 643 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 2476800..703dce7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ manipulate binary trees. Skip the tedious work of setting up test data, and dive straight into practising your algorithms. Heaps and BSTs (binary search trees) are also supported. +Check out the [documentation](http://binarytree.readthedocs.io) for more details. + ![IPython Demo](gifs/demo.gif) Binarytree can be used with [Graphviz](https://graphviz.org) and @@ -47,7 +49,7 @@ Binarytree uses the following class to represent a node: class Node: def __init__(self, value, left=None, right=None): - self.value = value # The node value (integer) + self.value = value # The node value (float/int/str) self.left = left # Left child self.right = right # Right child ``` @@ -330,7 +332,7 @@ print(root) # / \ # 4 5 -# First representation is was already shown above. +# First representation is already shown above. # All "null" nodes in each level are present. print(root.values) # [1, 2, None, 3, None, None, None, 4, 5] diff --git a/binarytree/__init__.py b/binarytree/__init__.py index 83c34ab..56de304 100644 --- a/binarytree/__init__.py +++ b/binarytree/__init__.py @@ -7,7 +7,10 @@ "build2", "get_index", "get_parent", + "number_to_letters", "__version__", + "NodeValue", + "NodeValueList", ] import heapq @@ -17,7 +20,8 @@ from subprocess import SubprocessError from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union -from graphviz import Digraph, ExecutableNotFound, nohtml +from graphviz import Digraph, nohtml +from graphviz.exceptions import ExecutableNotFound from pkg_resources import get_distribution from binarytree.exceptions import ( @@ -55,8 +59,16 @@ """ - -NodeValue = Union[float, int] +_NODE_VAL_TYPES = (float, int, str) +NodeValue = Any # Union[float, int, str] +NodeValueList = Union[ + List[Optional[float]], + List[Optional[int]], + List[Optional[str]], + List[float], + List[int], + List[str], +] @dataclass @@ -83,15 +95,15 @@ class Node: this class mentions "binary tree", it is referring to the current node and its descendants. - :param value: Node value (must be a number). - :type value: int | float + :param value: Node value (must be a float/int/str). + :type value: float | int | str :param left: Left child node (default: None). :type left: binarytree.Node | None :param right: Right child node (default: None). :type right: binarytree.Node | None :raise binarytree.exceptions.NodeTypeError: If left or right child node is not an instance of :class:`binarytree.Node`. - :raise binarytree.exceptions.NodeValueError: If node value is not an int or float. + :raise binarytree.exceptions.NodeValueError: If node value is invalid. """ def __init__( @@ -168,8 +180,7 @@ def __setattr__(self, attr: str, obj: Any) -> None: :type obj: object :raise binarytree.exceptions.NodeTypeError: If left or right child is not an instance of :class:`binarytree.Node`. - :raise binarytree.exceptions.NodeValueError: If node value is not a - number (e.g. int, float). + :raise binarytree.exceptions.NodeValueError: If node value is invalid. **Example**: @@ -178,20 +189,20 @@ def __setattr__(self, attr: str, obj: Any) -> None: >>> from binarytree import Node >>> >>> node = Node(1) - >>> node.left = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL + >>> node.left = [] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeTypeError: Left child must be a Node instance + binarytree.exceptions.NodeTypeError: Left child must be a Node instance .. doctest:: >>> from binarytree import Node >>> >>> node = Node(1) - >>> node.val = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL + >>> node.val = [] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeValueError: node value must be a float or int + binarytree.exceptions.NodeValueError: node value must be a float/int/str """ if attr == _ATTR_LEFT: if obj is not None and not isinstance(obj, Node): @@ -202,13 +213,13 @@ def __setattr__(self, attr: str, obj: Any) -> None: raise NodeTypeError("right child must be a Node instance") elif attr == _ATTR_VALUE: - if not isinstance(obj, (float, int)): - raise NodeValueError("node value must be a float or int") + if not isinstance(obj, _NODE_VAL_TYPES): + raise NodeValueError("node value must be a float/int/str") object.__setattr__(self, _ATTR_VAL, obj) elif attr == _ATTR_VAL: - if not isinstance(obj, (float, int)): - raise NodeValueError("node value must be a float or int") + if not isinstance(obj, _NODE_VAL_TYPES): + raise NodeValueError("node value must be a float/int/str") object.__setattr__(self, _ATTR_VALUE, obj) object.__setattr__(self, attr, obj) @@ -316,7 +327,7 @@ def __getitem__(self, index: int) -> "Node": >>> root[3] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: node missing at index 3 + binarytree.exceptions.NodeNotFoundError: node missing at index 3 """ if not isinstance(index, int) or index < 0: raise NodeIndexError("node index must be a non-negative int") @@ -383,7 +394,7 @@ def __setitem__(self, index: int, node: "Node") -> None: >>> root[0] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeModifyError: cannot modify the root node + binarytree.exceptions.NodeModifyError: cannot modify the root node .. doctest:: @@ -396,7 +407,7 @@ def __setitem__(self, index: int, node: "Node") -> None: >>> root[11] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: parent node missing at index 5 + binarytree.exceptions.NodeNotFoundError: parent node missing at index 5 .. doctest:: @@ -454,7 +465,7 @@ def __delitem__(self, index: int) -> None: >>> del root[0] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeModifyError: cannot delete the root node + binarytree.exceptions.NodeModifyError: cannot delete the root node .. doctest:: @@ -469,7 +480,7 @@ def __delitem__(self, index: int) -> None: >>> root[2] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: node missing at index 2 + binarytree.exceptions.NodeNotFoundError: node missing at index 2 """ if index == 0: raise NodeModifyError("cannot delete the root node") @@ -571,7 +582,7 @@ def graphviz(self, *args: Any, **kwargs: Any) -> Digraph: # pragma: no cover """Return a graphviz.Digraph_ object representing the binary tree. This method's positional and keyword arguments are passed directly into the - the Digraph's **__init__** method. + Digraph's **__init__** method. :return: graphviz.Digraph_ object representing the binary tree. :raise binarytree.exceptions.GraphvizImportError: If graphviz is not installed @@ -666,8 +677,7 @@ def validate(self) -> None: cyclic reference to a node in the binary tree. :raise binarytree.exceptions.NodeTypeError: If a node is not an instance of :class:`binarytree.Node`. - :raise binarytree.exceptions.NodeValueError: If a node value is not a - number (e.g. int, float). + :raise binarytree.exceptions.NodeValueError: If node value is invalid. **Example**: @@ -682,7 +692,7 @@ def validate(self) -> None: >>> root.validate() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeReferenceError: cyclic node reference at index 0 + binarytree.exceptions.NodeReferenceError: cyclic node reference at index 0 """ has_more_nodes = True nodes_seen = set() @@ -708,11 +718,11 @@ def validate(self) -> None: raise NodeTypeError( "invalid node instance at index {}".format(node_index) ) - if not isinstance(node.val, (float, int)): + if not isinstance(node.val, _NODE_VAL_TYPES): raise NodeValueError( "invalid node value at index {}".format(node_index) ) - if not isinstance(node.value, (float, int)): # pragma: no cover + if not isinstance(node.value, _NODE_VAL_TYPES): # pragma: no cover raise NodeValueError( "invalid node value at index {}".format(node_index) ) @@ -732,7 +742,7 @@ def equals(self, other: "Node") -> bool: :param other: Root of the other binary tree. :type other: binarytree.Node - :return: True if the binary trees a equal, False otherwise. + :return: True if the binary trees are equal, False otherwise. :rtype: bool """ stack1: List[Optional[Node]] = [self] @@ -1812,7 +1822,7 @@ def _build_bst_from_sorted_values(sorted_values: List[int]) -> Optional[Node]: """Recursively build a perfect BST from odd number of sorted values. :param sorted_values: Odd number of sorted values. - :type sorted_values: [int | float] + :type sorted_values: [float | int | str] :return: Root node of the BST. :rtype: binarytree.Node | None """ @@ -1842,8 +1852,21 @@ def _generate_random_leaf_count(height: int) -> int: return roll_1 + roll_2 or half_leaf_count -def _generate_random_node_values(height: int) -> List[NodeValue]: - """Return random node values for building binary trees. +def number_to_letters(number: int) -> str: + """Convert a positive integer to a string of uppercase letters. + + :param number: A positive integer. + :type number: int + :return: String of uppercase letters. + :rtype: str + """ + assert number >= 0, "number must be a positive integer" + quotient, remainder = divmod(number, 26) + return quotient * "Z" + chr(65 + remainder) + + +def _generate_random_numbers(height: int) -> List[int]: + """Return random numbers for building binary trees. :param height: Height of the binary tree. :type height: int @@ -1851,7 +1874,7 @@ def _generate_random_node_values(height: int) -> List[NodeValue]: :rtype: [int] """ max_node_count = 2 ** (height + 1) - 1 - node_values: List[NodeValue] = list(range(max_node_count)) + node_values = list(range(max_node_count)) random.shuffle(node_values) return node_values @@ -2060,7 +2083,7 @@ def get_index(root: Node, descendent: Node) -> int: >>> get_index(root.left, root.right) Traceback (most recent call last): ... - NodeReferenceError: given nodes are not in the same tree + binarytree.exceptions.NodeReferenceError: given nodes are not in the same tree """ if root is None: raise NodeTypeError("root must be a Node instance") @@ -2096,13 +2119,13 @@ def get_index(root: Node, descendent: Node) -> int: raise NodeReferenceError("given nodes are not in the same tree") -def get_parent(root: Node, child: Node) -> Optional[Node]: +def get_parent(root: Optional[Node], child: Optional[Node]) -> Optional[Node]: """Search the binary tree and return the parent of given child. :param root: Root node of the binary tree. - :type: binarytree.Node + :type: binarytree.Node | None :param child: Child node. - :rtype: binarytree.Node + :rtype: binarytree.Node | None :return: Parent node, or None if missing. :rtype: binarytree.Node | None @@ -2148,7 +2171,7 @@ def get_parent(root: Node, child: Node) -> Optional[Node]: return None -def build(values: List[NodeValue]) -> Optional[Node]: +def build(values: NodeValueList) -> Optional[Node]: """Build a tree from `list representation`_ and return its root node. .. _list representation: @@ -2159,7 +2182,7 @@ def build(values: List[NodeValue]) -> Optional[Node]: node). If a node is at index i, its left child is always at 2i + 1, right child at 2i + 2, and parent at floor((i - 1) / 2). "None" indicates absence of a node at that index. See example below for an illustration. - :type values: [int | float | None] + :type values: [float | int | str | None] :return: Root node of the binary tree. :rtype: binarytree.Node | None :raise binarytree.exceptions.NodeNotFoundError: If the list representation @@ -2189,7 +2212,7 @@ def build(values: List[NodeValue]) -> Optional[Node]: >>> root = build([None, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: parent node missing at index 0 + binarytree.exceptions.NodeNotFoundError: parent node missing at index 0 """ nodes = [None if v is None else Node(v) for v in values] @@ -2218,7 +2241,7 @@ def build2(values: List[NodeValue]) -> Optional[Node]: parent at floor((i - 1) / 2), but it allows for more compact lists as it does not hold "None"s between nodes in each level. See example below for an illustration. - :type values: [int | float | None] + :type values: [float | int | str | None] :return: Root node of the binary tree. :rtype: binarytree.Node | None :raise binarytree.exceptions.NodeNotFoundError: If the list representation @@ -2250,7 +2273,7 @@ def build2(values: List[NodeValue]) -> Optional[Node]: >>> root = build2([None, 1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeValueError: node value must be a float or int + binarytree.exceptions.NodeValueError: node value must be a float/int/str """ queue: Deque[Node] = deque() root: Optional[Node] = None @@ -2276,7 +2299,11 @@ def build2(values: List[NodeValue]) -> Optional[Node]: return root -def tree(height: int = 3, is_perfect: bool = False) -> Optional[Node]: +def tree( + height: int = 3, + is_perfect: bool = False, + letters: bool = False, +) -> Optional[Node]: """Generate a random binary tree and return its root node. :param height: Height of the tree (default: 3, range: 0 - 9 inclusive). @@ -2285,6 +2312,9 @@ def tree(height: int = 3, is_perfect: bool = False) -> Optional[Node]: with all levels filled is returned. If set to False, a perfect binary tree may still be generated by chance. :type is_perfect: bool + :param letters: If set to True (default: False), uppercase alphabet letters are + used for node values instead of numbers. + :type letters: bool :return: Root node of the binary tree. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. @@ -2318,10 +2348,14 @@ def tree(height: int = 3, is_perfect: bool = False) -> Optional[Node]: >>> root = tree(height=20) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - TreeHeightError: height must be an int between 0 - 9 + binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) - values = _generate_random_node_values(height) + numbers = _generate_random_numbers(height) + values: NodeValueList = ( + list(map(number_to_letters, numbers)) if letters else numbers + ) + if is_perfect: return build(values) @@ -2350,7 +2384,11 @@ def tree(height: int = 3, is_perfect: bool = False) -> Optional[Node]: return root_node -def bst(height: int = 3, is_perfect: bool = False) -> Optional[Node]: +def bst( + height: int = 3, + is_perfect: bool = False, + letters: bool = False, +) -> Optional[Node]: """Generate a random BST (binary search tree) and return its root node. :param height: Height of the BST (default: 3, range: 0 - 9 inclusive). @@ -2359,6 +2397,9 @@ def bst(height: int = 3, is_perfect: bool = False) -> Optional[Node]: levels filled is returned. If set to False, a perfect BST may still be generated by chance. :type is_perfect: bool + :param letters: If set to True (default: False), uppercase alphabet letters are + used for node values instead of numbers. + :type letters: bool :return: Root node of the BST. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. @@ -2383,13 +2424,16 @@ def bst(height: int = 3, is_perfect: bool = False) -> Optional[Node]: >>> root = bst(10) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - TreeHeightError: height must be an int between 0 - 9 + binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) if is_perfect: return _generate_perfect_bst(height) - values = _generate_random_node_values(height) + numbers = _generate_random_numbers(height) + values: NodeValueList = ( + list(map(number_to_letters, numbers)) if letters else numbers + ) leaf_count = _generate_random_leaf_count(height) root_node = Node(values.pop(0)) @@ -2417,7 +2461,10 @@ def bst(height: int = 3, is_perfect: bool = False) -> Optional[Node]: def heap( - height: int = 3, is_max: bool = True, is_perfect: bool = False + height: int = 3, + is_max: bool = True, + is_perfect: bool = False, + letters: bool = False, ) -> Optional[Node]: """Generate a random heap and return its root node. @@ -2431,6 +2478,9 @@ def heap( levels filled is returned. If set to False, a perfect heap may still be generated by chance. :type is_perfect: bool + :param letters: If set to True (default: False), uppercase alphabet letters are + used for node values instead of numbers. + :type letters: bool :return: Root node of the heap. :rtype: binarytree.Node :raise binarytree.exceptions.TreeHeightError: If height is invalid. @@ -2479,20 +2529,21 @@ def heap( >>> root = heap(-1) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - TreeHeightError: height must be an int between 0 - 9 + binarytree.exceptions.TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) - values = _generate_random_node_values(height) + values = _generate_random_numbers(height) if not is_perfect: - # Randomly cut some of the leaf nodes away + # Randomly cut some leaf nodes away random_cut = random.randint(2**height, len(values)) values = values[:random_cut] if is_max: negated = [-v for v in values] heapq.heapify(negated) - return build([-v for v in negated]) + values = [-v for v in negated] else: heapq.heapify(values) - return build(values) + + return build(list(map(number_to_letters, values)) if letters else values) diff --git a/binarytree/exceptions.py b/binarytree/exceptions.py index 0676e24..3b450dc 100644 --- a/binarytree/exceptions.py +++ b/binarytree/exceptions.py @@ -23,7 +23,7 @@ class NodeTypeError(BinaryTreeError): class NodeValueError(BinaryTreeError): - """Node value was not a number (e.g. int, float).""" + """Node value was not a number (e.g. float, int, str).""" class TreeHeightError(BinaryTreeError): diff --git a/docs/overview.rst b/docs/overview.rst index 066622c..b1447d6 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -8,7 +8,7 @@ Binarytree uses the following class to represent a node: class Node: def __init__(self, value, left=None, right=None): - self.value = value # The node value (integer) + self.value = value # The node value (float/int/str) self.left = left # Left child self.right = right # Right child @@ -301,7 +301,7 @@ the `indexing properties`_: 4 5 - >>> # First representation is was already shown above. + >>> # First representation is already shown above. >>> # All "null" nodes in each level are present. >>> root.values [1, 2, None, 3, None, None, None, 4, 5] diff --git a/tests/test_tree.py b/tests/test_tree.py index e574900..ada3d5a 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -2,10 +2,21 @@ import copy import random +from typing import Any, List, Optional import pytest -from binarytree import Node, bst, build, build2, get_index, get_parent, heap, tree +from binarytree import ( + Node, + bst, + build, + build2, + get_index, + get_parent, + heap, + number_to_letters, + tree, +) from binarytree.exceptions import ( NodeIndexError, NodeModifyError, @@ -72,10 +83,10 @@ """ +EMPTY_LIST: List[Optional[int]] = [] -# noinspection PyTypeChecker -def test_node_set_attributes(): +def test_node_init_and_setattr_with_integers() -> None: root = Node(1) assert root.left is None assert root.right is None @@ -118,48 +129,132 @@ def test_node_set_attributes(): assert root.left.right is last_node assert repr(root.left.right) == "Node(4)" - with pytest.raises(NodeValueError) as err: - # noinspection PyTypeChecker - Node("this_is_not_an_integer") - assert str(err.value) == "node value must be a float or int" - with pytest.raises(NodeTypeError) as err: - # noinspection PyTypeChecker - Node(1, "this_is_not_a_node") - assert str(err.value) == "left child must be a Node instance" +def test_node_init_and_setattr_with_floats() -> None: + root = Node(1.5) + assert root.left is None + assert root.right is None + assert root.val == 1.5 + assert root.value == 1.5 + assert repr(root) == "Node(1.5)" - with pytest.raises(NodeTypeError) as err: - # noinspection PyTypeChecker - Node(1, Node(1), "this_is_not_a_node") - assert str(err.value) == "right child must be a Node instance" + root.value = 2.5 + assert root.value == 2.5 + assert root.val == 2.5 + assert repr(root) == "Node(2.5)" - with pytest.raises(NodeValueError) as err: - root.val = "this_is_not_an_integer" - assert root.val == 1 - assert str(err.value) == "node value must be a float or int" + root.val = 1.5 + assert root.value == 1.5 + assert root.val == 1.5 + assert repr(root) == "Node(1.5)" - with pytest.raises(NodeValueError) as err: - root.value = "this_is_not_an_integer" - assert root.value == 1 - assert str(err.value) == "node value must be a float or int" + left_child = Node(2.5) + root.left = left_child + assert root.left is left_child + assert root.right is None + assert root.val == 1.5 + assert root.left.left is None + assert root.left.right is None + assert root.left.val == 2.5 + assert repr(left_child) == "Node(2.5)" + + right_child = Node(3.5) + root.right = right_child + assert root.left is left_child + assert root.right is right_child + assert root.val == 1.5 + assert root.right.left is None + assert root.right.right is None + assert root.right.val == 3.5 + assert repr(right_child) == "Node(3.5)" + + last_node = Node(4.5) + left_child.right = last_node + assert root.left.right is last_node + assert repr(root.left.right) == "Node(4.5)" + + +def test_node_init_and_setattr_with_letters() -> None: + root = Node("A") + assert root.left is None + assert root.right is None + assert root.val == "A" + assert root.value == "A" + assert repr(root) == "Node(A)" + + root.value = "B" + assert root.value == "B" + assert root.val == "B" + assert repr(root) == "Node(B)" + + root.val = "A" + assert root.value == "A" + assert root.val == "A" + assert repr(root) == "Node(A)" + + left_child = Node("B") + root.left = left_child + assert root.left is left_child + assert root.right is None + assert root.val == "A" + assert root.left.left is None + assert root.left.right is None + assert root.left.val == "B" + assert repr(left_child) == "Node(B)" - with pytest.raises(NodeTypeError) as err: - root.left = "this_is_not_a_node" + right_child = Node("C") + root.right = right_child assert root.left is left_child - assert str(err.value) == "left child must be a Node instance" + assert root.right is right_child + assert root.val == "A" + assert root.right.left is None + assert root.right.right is None + assert root.right.val == "C" + assert repr(right_child) == "Node(C)" + + last_node = Node("D") + left_child.right = last_node + assert root.left.right is last_node + assert repr(root.left.right) == "Node(D)" - with pytest.raises(NodeTypeError) as err: - root.right = "this_is_not_a_node" + +def test_node_init_and_setattr_error_cases() -> None: + root, left_child, right_child = Node(1), Node(2), Node(3) + root.left = left_child + root.right = right_child + + with pytest.raises(NodeValueError) as err1: + Node(EMPTY_LIST) + assert str(err1.value) == "node value must be a float/int/str" + + with pytest.raises(NodeValueError) as err2: + Node(1).val = EMPTY_LIST + assert str(err2.value) == "node value must be a float/int/str" + + with pytest.raises(NodeTypeError) as err3: + Node(1, "this_is_not_a_node") # type: ignore + assert str(err3.value) == "left child must be a Node instance" + + with pytest.raises(NodeTypeError) as err4: + Node(1, Node(1), "this_is_not_a_node") # type: ignore + assert str(err4.value) == "right child must be a Node instance" + + with pytest.raises(NodeTypeError) as err5: + root.left = "this_is_not_a_node" # type: ignore + assert root.left is left_child + assert str(err5.value) == "left child must be a Node instance" + + with pytest.raises(NodeTypeError) as err6: + root.right = "this_is_not_a_node" # type: ignore assert root.right is right_child - assert str(err.value) == "right child must be a Node instance" + assert str(err6.value) == "right child must be a Node instance" -# noinspection PyTypeChecker -def test_tree_equals(): +def test_tree_equals_with_integers() -> None: root1 = Node(1) root2 = Node(1) - assert root1.equals(None) is False - assert root1.equals(1) is False + assert root1.equals(None) is False # type: ignore + assert root1.equals(1) is False # type: ignore assert root1.equals(Node(2)) is False assert root1.equals(root2) is True assert root2.equals(root1) is True @@ -189,33 +284,119 @@ def test_tree_equals(): assert root2.equals(root1) is True -def test_tree_clone(): +def test_tree_equals_with_floats() -> None: + root1 = Node(1.5) + root2 = Node(1.5) + assert root1.equals(None) is False # type: ignore + assert root1.equals(1.5) is False # type: ignore + assert root1.equals(Node(2.5)) is False + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.left = Node(2.5) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.left = Node(2.5) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right = Node(3.5) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right = Node(3.5) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right.left = Node(4.5) + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right.left = Node(4.5) + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + +def test_tree_equals_with_letters() -> None: + root1 = Node("A") + root2 = Node("A") + assert root1.equals(None) is False # type: ignore + assert root1.equals("A") is False # type: ignore + assert root1.equals(Node("B")) is False + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.left = Node("B") + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.left = Node("B") + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right = Node("C") + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right = Node("C") + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + root1.right.left = Node("D") + assert root1.equals(root2) is False + assert root2.equals(root1) is False + + root2.right.left = Node("D") + assert root1.equals(root2) is True + assert root2.equals(root1) is True + + +def test_tree_clone_with_numbers() -> None: + for _ in range(REPETITIONS): + root = tree(letters=False) + assert root is not None + clone = root.clone() + assert root.values == clone.values + assert root.equals(clone) + assert clone.equals(root) + assert root.properties == clone.properties + + +def test_tree_clone_with_letters() -> None: for _ in range(REPETITIONS): - root = tree() + root = tree(letters=True) + assert root is not None clone = root.clone() assert root.values == clone.values assert root.equals(clone) assert clone.equals(root) + assert root.properties == clone.properties -# noinspection PyUnresolvedReferences -def test_list_representation(): - root = build([]) +def test_list_representation_1() -> None: + root = build(EMPTY_LIST) assert root is None root = build([1]) + assert root is not None assert root.val == 1 assert root.left is None assert root.right is None root = build([1, 2]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 assert root.right is None root = build([1, 2, 3]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 + assert root.right is not None assert root.right.val == 3 assert root.left.left is None assert root.left.right is None @@ -223,10 +404,14 @@ def test_list_representation(): assert root.right.right is None root = build([1, 2, 3, None, 4]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 + assert root.right is not None assert root.right.val == 3 assert root.left.left is None + assert root.left.right is not None assert root.left.right.val == 4 assert root.right.left is None assert root.right.right is None @@ -264,28 +449,37 @@ def test_list_representation(): for _ in range(REPETITIONS): t1 = tree() + assert t1 is not None + t2 = build(t1.values) + assert t2 is not None + assert t1.values == t2.values -# noinspection PyUnresolvedReferences -def test_list_representation2(): - root = build2([]) +def test_list_representation_2() -> None: + root = build2(EMPTY_LIST) assert root is None root = build2([1]) + assert root is not None assert root.val == 1 assert root.left is None assert root.right is None root = build2([1, 2]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 assert root.right is None root = build2([1, 2, 3]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 + assert root.right is not None assert root.right.val == 3 assert root.left.left is None assert root.left.right is None @@ -293,10 +487,14 @@ def test_list_representation2(): assert root.right.right is None root = build2([1, 2, 3, None, 4]) + assert root is not None assert root.val == 1 + assert root.left is not None assert root.left.val == 2 + assert root.right is not None assert root.right.val == 3 assert root.left.left is None + assert root.left.right is not None assert root.left.right.val == 4 assert root.right.left is None assert root.right.right is None @@ -304,18 +502,27 @@ def test_list_representation2(): assert root.left.right.right is None root = build2([1, None, 2, 3, 4]) + assert root is not None assert root.val == 1 assert root.left is None + assert root.right is not None assert root.right.val == 2 + assert root.right.left is not None assert root.right.left.val == 3 + assert root.right.right is not None assert root.right.right.val == 4 root = build2([2, 5, None, 3, None, 1, 4]) + assert root is not None assert root.val == 2 + assert root.left is not None assert root.left.val == 5 assert root.right is None + assert root.left.left is not None assert root.left.left.val == 3 + assert root.left.left.left is not None assert root.left.left.left.val == 1 + assert root.left.left.right is not None assert root.left.left.right.val == 4 with pytest.raises(NodeValueError): @@ -352,11 +559,15 @@ def test_list_representation2(): for _ in range(REPETITIONS): t1 = tree() + assert t1 is not None + t2 = build2(t1.values2) + assert t2 is not None + assert t1.values2 == t2.values2 -def test_tree_get_node(): +def test_tree_get_node_by_level_order_index() -> None: root = Node(1) root.left = Node(2) root.right = Node(3) @@ -372,16 +583,16 @@ def test_tree_get_node(): assert root[9] is root.left.right.left for index in [5, 6, 7, 8, 10]: - with pytest.raises(NodeNotFoundError) as err: + with pytest.raises(NodeNotFoundError) as err1: assert root[index] - assert str(err.value) == "node missing at index {}".format(index) + assert str(err1.value) == "node missing at index {}".format(index) - with pytest.raises(NodeIndexError) as err: + with pytest.raises(NodeIndexError) as err2: assert root[-1] - assert str(err.value) == "node index must be a non-negative int" + assert str(err2.value) == "node index must be a non-negative int" -def test_tree_set_node(): +def test_tree_set_node_by_level_order_index() -> None: root = Node(1) root.left = Node(2) root.right = Node(3) @@ -393,17 +604,17 @@ def test_tree_set_node(): new_node_2 = Node(8) new_node_3 = Node(9) - with pytest.raises(NodeModifyError) as err: + with pytest.raises(NodeModifyError) as err1: root[0] = new_node_1 - assert str(err.value) == "cannot modify the root node" + assert str(err1.value) == "cannot modify the root node" - with pytest.raises(NodeIndexError) as err: + with pytest.raises(NodeIndexError) as err2: root[-1] = new_node_1 - assert str(err.value) == "node index must be a non-negative int" + assert str(err2.value) == "node index must be a non-negative int" - with pytest.raises(NodeNotFoundError) as err: + with pytest.raises(NodeNotFoundError) as err3: root[100] = new_node_1 - assert str(err.value) == "parent node missing at index 49" + assert str(err3.value) == "parent node missing at index 49" root[10] = new_node_1 assert root.val == 1 @@ -429,7 +640,7 @@ def test_tree_set_node(): assert root.right is new_node_2 -def test_tree_del_node(): +def test_tree_delete_node_by_level_order_index() -> None: root = Node(1) root.left = Node(2) root.right = Node(3) @@ -437,21 +648,21 @@ def test_tree_del_node(): root.left.right = Node(5) root.left.right.left = Node(6) - with pytest.raises(NodeModifyError) as err: + with pytest.raises(NodeModifyError) as err1: del root[0] - assert str(err.value) == "cannot delete the root node" + assert str(err1.value) == "cannot delete the root node" - with pytest.raises(NodeIndexError) as err: + with pytest.raises(NodeIndexError) as err2: del root[-1] - assert str(err.value) == "node index must be a non-negative int" + assert str(err2.value) == "node index must be a non-negative int" - with pytest.raises(NodeNotFoundError) as err: + with pytest.raises(NodeNotFoundError) as err3: del root[10] - assert str(err.value) == "no node to delete at index 10" + assert str(err3.value) == "no node to delete at index 10" - with pytest.raises(NodeNotFoundError) as err: + with pytest.raises(NodeNotFoundError) as err4: del root[100] - assert str(err.value) == "no node to delete at index 100" + assert str(err4.value) == "no node to delete at index 100" del root[3] assert root.left.left is None @@ -489,7 +700,7 @@ def test_tree_del_node(): assert root.size == 1 -def test_tree_print_no_index(): +def test_tree_print_with_integers_no_index() -> None: for printer in [builtin_print, pprint_default]: lines = printer([1]) assert lines == ["1"] @@ -521,7 +732,7 @@ def test_tree_print_no_index(): ] -def test_tree_print_with_index(): +def test_tree_print_with_integers_with_index() -> None: lines = pprint_with_index([1]) assert lines == ["0:1"] lines = pprint_with_index([1, 2]) @@ -564,9 +775,84 @@ def test_tree_print_with_index(): ] -def test_tree_validate(): +def test_tree_print_with_letters_no_index() -> None: + for printer in [builtin_print, pprint_default]: + lines = printer(["A"]) + assert lines == ["A"] + lines = printer(["A", "B"]) + assert lines == [" A", " /", "B"] + lines = printer(["A", None, "C"]) + assert lines == ["A", " \\", " C"] + lines = printer(["A", "B", "C"]) + assert lines == [" A", " / \\", "B C"] + lines = printer(["A", "B", "C", None, "E"]) + assert lines == [" __A", " / \\", "B C", " \\", " E"] + lines = printer(["A", "B", "C", None, "E", "F"]) + assert lines == [" __A__", " / \\", "B C", " \\ /", " E F"] + lines = printer(["A", "B", "C", None, "E", "F", "G"]) + assert lines == [ + " __A__", + " / \\", + "B C", + " \\ / \\", + " E F G", + ] + lines = printer(["A", "B", "C", "D", "E", "F", "G"]) + assert lines == [ + " __A__", + " / \\", + " B C", + " / \\ / \\", + "D E F G", + ] + + +def test_tree_print_with_letters_with_index() -> None: + lines = pprint_with_index(["A"]) + assert lines == ["0:A"] + lines = pprint_with_index(["A", "B"]) + assert lines == [" _0:A", " /", "1:B"] + lines = pprint_with_index(["A", None, "C"]) + assert lines == ["0:A_", " \\", " 2:C"] + lines = pprint_with_index(["A", "B", "C"]) + assert lines == [" _0:A_", " / \\", "1:B 2:C"] + lines = pprint_with_index(["A", "B", "C", None, "E"]) + assert lines == [ + " _____0:A_", + " / \\", + "1:B_ 2:C", + " \\", + " 4:E", + ] + lines = pprint_with_index(["A", "B", "C", None, "E", "F"]) + assert lines == [ + " _____0:A_____", + " / \\", + "1:B_ _2:C", + " \\ /", + " 4:E 5:F", + ] + lines = pprint_with_index(["A", "B", "C", None, "E", "F", "G"]) + assert lines == [ + " _____0:A_____", + " / \\", + "1:B_ _2:C_", + " \\ / \\", + " 4:E 5:F 6:G", + ] + lines = pprint_with_index(["A", "B", "C", "D", "E", "F", "G"]) + assert lines == [ + " _____0:A_____", + " / \\", + " _1:B_ _2:C_", + " / \\ / \\", + "3:D 4:E 5:F 6:G", + ] + + +def test_tree_validate() -> None: class TestNode(Node): - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value: Any) -> None: object.__setattr__(self, attr, value) root = Node(1) @@ -590,27 +876,73 @@ def __setattr__(self, attr, value): root.validate() # Should pass root = TestNode(1) - root.left = "not_a_node" - with pytest.raises(NodeTypeError) as err: + root.left = "not_a_node" # type: ignore + with pytest.raises(NodeTypeError) as err1: root.validate() - assert str(err.value) == "invalid node instance at index 1" + assert str(err1.value) == "invalid node instance at index 1" root = TestNode(1) root.right = TestNode(2) - root.right.val = "not_an_integer" - with pytest.raises(NodeValueError) as err: + root.right.val = EMPTY_LIST + with pytest.raises(NodeValueError) as err2: root.validate() - assert str(err.value) == "invalid node value at index 2" + assert str(err2.value) == "invalid node value at index 2" root = TestNode(1) root.left = TestNode(2) root.left.right = root - with pytest.raises(NodeReferenceError) as err: + with pytest.raises(NodeReferenceError) as err3: + root.validate() + assert str(err3.value) == "cyclic reference at Node(1) (level-order index 4)" + + +def test_tree_validate_with_letters() -> None: + class TestNode(Node): + def __setattr__(self, attr: str, value: Any) -> None: + object.__setattr__(self, attr, value) + + root = Node("A") + root.validate() # Should pass + + root = Node("A") + root.left = Node("B") + root.validate() # Should pass + + root = Node("A") + root.left = Node("B") + root.right = Node(3) + root.validate() # Should pass + + root = Node("A") + root.left = Node("B") + root.right = Node(3) + root.left.left = Node(4) + root.left.right = Node(5) + root.left.right.left = Node(6) + root.validate() # Should pass + + root = TestNode("A") + root.left = "not_a_node" # type: ignore + with pytest.raises(NodeTypeError) as err1: + root.validate() + assert str(err1.value) == "invalid node instance at index 1" + + root = TestNode("A") + root.right = TestNode("B") + root.right.val = EMPTY_LIST + with pytest.raises(NodeValueError) as err2: + root.validate() + assert str(err2.value) == "invalid node value at index 2" + + root = TestNode("A") + root.left = TestNode("B") + root.left.right = root + with pytest.raises(NodeReferenceError) as err3: root.validate() - assert str(err.value) == "cyclic reference at Node(1) (level-order index 4)" + assert str(err3.value) == "cyclic reference at Node(A) (level-order index 4)" -def test_tree_properties(): +def test_tree_properties() -> None: root = Node(1) assert root.properties == { "height": 0, @@ -850,7 +1182,7 @@ def test_tree_properties(): assert root.size == len(root) == 7 -def test_tree_traversal(): +def test_tree_traversal() -> None: n1 = Node(1) assert n1.levels == [[n1]] assert n1.leaves == [n1] @@ -890,13 +1222,15 @@ def test_tree_traversal(): assert n1.levelorder == [n1, n2, n3, n4, n5] -def test_tree_generation(): +def test_tree_generation() -> None: for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: - tree(height=invalid_height) + tree(height=invalid_height) # type: ignore assert str(err.value) == "height must be an int between 0 - 9" root = tree(height=0) + assert root is not None + root.validate() assert root.height == 0 assert root.left is None @@ -905,13 +1239,19 @@ def test_tree_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) + root = tree(random_height) + assert root is not None + root.validate() assert root.height == random_height for _ in range(REPETITIONS): random_height = random.randint(1, 9) + root = tree(random_height, is_perfect=True) + assert root is not None + root.validate() assert root.height == random_height assert root.is_perfect is True @@ -919,13 +1259,14 @@ def test_tree_generation(): assert root.is_strict is True -def test_bst_generation(): +def test_bst_generation() -> None: for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: - bst(height=invalid_height) + bst(height=invalid_height) # type: ignore assert str(err.value) == "height must be an int between 0 - 9" root = bst(height=0) + assert root is not None root.validate() assert root.height == 0 assert root.left is None @@ -935,6 +1276,15 @@ def test_bst_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) root = bst(random_height) + assert root is not None + root.validate() + assert root.is_bst is True + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = bst(random_height, letters=True) + assert root is not None root.validate() assert root.is_bst is True assert root.height == random_height @@ -942,11 +1292,11 @@ def test_bst_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) root = bst(random_height, is_perfect=True) + assert root is not None root.validate() assert root.height == random_height if not root.is_bst: - print(root) raise Exception("boo") assert root.is_bst is True @@ -954,14 +1304,30 @@ def test_bst_generation(): assert root.is_balanced is True assert root.is_strict is True + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = bst(random_height, letters=True, is_perfect=True) + assert root is not None + root.validate() + assert root.height == random_height -def test_heap_generation(): + if not root.is_bst: + raise Exception("boo") + + assert root.is_bst is True + assert root.is_perfect is True + assert root.is_balanced is True + assert root.is_strict is True + + +def test_heap_generation() -> None: for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: - heap(height=invalid_height) + heap(height=invalid_height) # type: ignore assert str(err.value) == "height must be an int between 0 - 9" root = heap(height=0) + assert root is not None root.validate() assert root.height == 0 assert root.left is None @@ -971,6 +1337,16 @@ def test_heap_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) root = heap(random_height, is_max=True) + assert root is not None + root.validate() + assert root.is_max_heap is True + assert root.is_min_heap is False + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, letters=True, is_max=True) + assert root is not None root.validate() assert root.is_max_heap is True assert root.is_min_heap is False @@ -979,6 +1355,16 @@ def test_heap_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) root = heap(random_height, is_max=False) + assert root is not None + root.validate() + assert root.is_max_heap is False + assert root.is_min_heap is True + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, letters=True, is_max=False) + assert root is not None root.validate() assert root.is_max_heap is False assert root.is_min_heap is True @@ -987,6 +1373,19 @@ def test_heap_generation(): for _ in range(REPETITIONS): random_height = random.randint(1, 9) root = heap(random_height, is_perfect=True) + assert root is not None + root.validate() + assert root.is_max_heap is True + assert root.is_min_heap is False + assert root.is_perfect is True + assert root.is_balanced is True + assert root.is_strict is True + assert root.height == random_height + + for _ in range(REPETITIONS): + random_height = random.randint(1, 9) + root = heap(random_height, letters=True, is_perfect=True) + assert root is not None root.validate() assert root.is_max_heap is True assert root.is_min_heap is False @@ -996,7 +1395,7 @@ def test_heap_generation(): assert root.height == random_height -def test_heap_float_values(): +def test_heap_float_values() -> None: root = Node(1.0) root.left = Node(0.5) root.right = Node(1.5) @@ -1034,9 +1433,11 @@ def test_heap_float_values(): " 5.0", ] + +def test_heap_float_values_builders() -> None: for builder in [tree, bst, heap]: for _ in range(REPETITIONS): - root = builder() + root = builder() # type: ignore root_copy = copy.deepcopy(root) for node in root: @@ -1059,7 +1460,7 @@ def test_heap_float_values(): assert root.size == root_copy.size -def test_get_index(): +def test_get_index_utility_function() -> None: root = Node(0) root.left = Node(1) root.right = Node(2) @@ -1072,23 +1473,20 @@ def test_get_index(): assert get_index(root, root.left.left) == 3 assert get_index(root, root.right.right) == 6 - with pytest.raises(NodeReferenceError) as err: + with pytest.raises(NodeReferenceError) as err1: get_index(root.left, root.right) - assert str(err.value) == "given nodes are not in the same tree" + assert str(err1.value) == "given nodes are not in the same tree" - with pytest.raises(NodeTypeError) as err: - # noinspection PyTypeChecker - get_index(root, None) - assert str(err.value) == "descendent must be a Node instance" + with pytest.raises(NodeTypeError) as err2: + get_index(root, None) # type: ignore + assert str(err2.value) == "descendent must be a Node instance" - with pytest.raises(NodeTypeError) as err: - # noinspection PyTypeChecker - get_index(None, root.left) - assert str(err.value) == "root must be a Node instance" + with pytest.raises(NodeTypeError) as err3: + get_index(None, root.left) # type: ignore + assert str(err3.value) == "root must be a Node instance" -# noinspection PyTypeChecker -def test_get_parent(): +def test_get_parent_utility_function() -> None: root = Node(0) root.left = Node(1) root.right = Node(2) @@ -1105,7 +1503,7 @@ def test_get_parent(): assert get_parent(root, None) is None -def test_svg_generation(): +def test_svg_generation() -> None: root = Node(0) assert root.svg() == EXPECTED_SVG_XML_SINGLE_NODE @@ -1114,3 +1512,24 @@ def test_svg_generation(): root.left.left = Node(3) root.right.right = Node(4) assert root.svg() == EXPECTED_SVG_XML_MULTIPLE_NODES + + +def test_number_to_letters_utility_function() -> None: + with pytest.raises(AssertionError): + number_to_letters(-1) + + assert number_to_letters(0) == "A" + assert number_to_letters(1) == "B" + assert number_to_letters(25) == "Z" + assert number_to_letters(26) == "ZA" + assert number_to_letters(51) == "ZZ" + assert number_to_letters(52) == "ZZA" + + for _ in range(REPETITIONS): + num1 = random.randint(0, 1000) + num2 = random.randint(0, 1000) + str1 = number_to_letters(num1) + str2 = number_to_letters(num2) + assert (num1 < num2) == (str1 < str2) + assert (num1 > num2) == (str1 > str2) + assert (num1 == num2) == (str1 == str2) diff --git a/tests/utils.py b/tests/utils.py index 1f5de92..4b816de 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals import sys +from typing import Any, List try: # noinspection PyCompatibility @@ -8,45 +9,51 @@ except ImportError: from io import StringIO -from binarytree import build +from binarytree import NodeValueList, build -class CaptureOutput(list): +class CaptureOutput(List[str]): """Context manager to catch stdout.""" - def __enter__(self): + def __enter__(self) -> "CaptureOutput": self._original_stdout = sys.stdout self._temp_stdout = StringIO() sys.stdout = self._temp_stdout return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: lines = self._temp_stdout.getvalue().splitlines() self.extend(line.rstrip() for line in lines) sys.stdout = self._original_stdout -def pprint_default(values): +def pprint_default(values: NodeValueList) -> List[str]: """Helper function for testing Node.pprint with default arguments.""" root = build(values) + assert root is not None + with CaptureOutput() as output: root.pprint(index=False, delimiter="-") assert output[0] == "" and output[-1] == "" return [line for line in output if line != ""] -def pprint_with_index(values): +def pprint_with_index(values: NodeValueList) -> List[str]: """Helper function for testing Node.pprint with indexes.""" root = build(values) + assert root is not None + with CaptureOutput() as output: root.pprint(index=True, delimiter=":") assert output[0] == "" and output[-1] == "" return [line for line in output if line != ""] -def builtin_print(values): +def builtin_print(values: NodeValueList) -> List[str]: """Helper function for testing builtin print on Node.""" root = build(values) + assert root is not None + with CaptureOutput() as output: print(root) assert output[0] == "" and output[-1] == ""