diff --git a/.travis.yml b/.travis.yml index 4ce66bf..9bc250f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,15 +6,12 @@ python: - 3.5 - 3.6 install: - - pip install coverage - - pip install pytest - - pip install pytest-cov - - pip install python-coveralls - - pip install flake8 - - python setup.py install + - pip install flake8 mock pytest pytest-cov python-coveralls sphinx sphinx_rtd_theme + - pip install . script: - - python -m flake8 binarytree/__init__.py - - python -m doctest binarytree/__init__.py - - py.test tests.py --cov=binarytree + - python -m flake8 + - python -m sphinx -b doctest docs docs/_build + - python -m sphinx -b html -W docs docs/_build + - py.test -s -v --cov=binarytree after_success: - coveralls diff --git a/MANIFEST.in b/MANIFEST.in index 0c73842..95dea49 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include README.rst LICENSE +prune tests diff --git a/README.rst b/README.rst index 95d4a9a..737b677 100644 --- a/README.rst +++ b/README.rst @@ -37,25 +37,22 @@ Are you studying binary trees for your next exam, assignment or technical interv **Binarytree** is a Python library which provides a simple API to generate, visualize, inspect and manipulate binary trees. It allows you to 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. +tedious work of setting up test data, and dive straight into practising your +algorithms. Heaps and BSTs (binary search trees) are also supported. Announcements ============= -* **Binarytree** has been completely overhauled in version `3.0`_! +* **Binarytree** version `4.0`_ is now out! * Please see the releases_ page for details on the latest updates. -.. _3.0: https://github.com/joowani/binarytree/releases/tag/3.0.0 +.. _4.0: https://github.com/joowani/binarytree/releases/tag/4.0.0 .. _releases: https://github.com/joowani/binarytree/releases Requirements ============ - Python 2.7, 3.4, 3.5 or 3.6 -- Pip_ installer - -.. _Pip: https://pip.pypa.io Installation ============ @@ -66,19 +63,17 @@ To install a stable version from PyPi_: ~$ pip install binarytree - To install the latest version directly from GitHub_: .. code-block:: bash ~$ pip install -e git+git@github.com:joowani/binarytree.git@master#egg=binarytree -You may need to use ``sudo`` depending on your environment setup. +You may need to use ``sudo`` depending on your environment. .. _PyPi: https://pypi.python.org/pypi/binarytree .. _GitHub: https://github.com/joowani/binarytree - Getting Started =============== @@ -93,7 +88,6 @@ By default, **binarytree** uses the following class to represent a node: self.left = left # Left child self.right = right # Right child - Generate and pretty-print various types of binary trees: .. code-block:: python @@ -164,7 +158,6 @@ Use the `binarytree.Node`_ class to build your own trees: # 4 # - Inspect tree properties: .. code-block:: python @@ -296,7 +289,6 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: # \ # 2-3 - Traverse the trees using different algorithms: .. code-block:: python @@ -329,19 +321,20 @@ Traverse the trees using different algorithms: >>> root.levelorder [Node(1), Node(2), Node(3), Node(4), Node(5)] + >>> list(root) # Equivalent to root.levelorder + [Node(1), Node(2), Node(3), Node(4), Node(5)] `List representations`_ are also supported: -.. _List representations: - https://en.wikipedia.org/wiki/Binary_tree#Arrays - +.. _List representations: https://en.wikipedia.org/wiki/Binary_tree#Arrays .. code-block:: python >>> from binarytree import build >>> >>> # Build a tree from list representation - >>> root = build([7, 3, 2, 6, 9, None, 1, 5, 8]) + >>> values = [7, 3, 2, 6, 9, None, 1, 5, 8] + >>> root = build(values) >>> print(root) # # __7 @@ -353,19 +346,16 @@ Traverse the trees using different algorithms: # 5 8 # >>> # Convert the tree back to list representation - >>> list(root) + >>> root.values [7, 3, 2, 6, 9, None, 1, 5, 8] - - Check out the documentation_ for more details! .. _documentation: http://binarytree.readthedocs.io/en/latest/index.html - Contributing ============ Please have a look at this page_ before submitting a pull request. Thanks! -.. _page: http://binarytree.readthedocs.io/en/latest/contributing.html \ No newline at end of file +.. _page: http://binarytree.readthedocs.io/en/latest/contributing.html diff --git a/binarytree/__init__.py b/binarytree/__init__.py index 0055bef..6a57980 100644 --- a/binarytree/__init__.py +++ b/binarytree/__init__.py @@ -4,24 +4,25 @@ import heapq import random +import numbers from binarytree.exceptions import ( - InvalidNodeValueError, - InvalidNodeIndexError, - InvalidNodeTypeError, - OperationForbiddenError, + TreeHeightError, + NodeValueError, + NodeIndexError, + NodeTypeError, + NodeModifyError, NodeNotFoundError, - InvalidTreeHeightError, - CyclicNodeReferenceError, + NodeReferenceError, ) def _is_balanced(root): """Return the height if the binary tree is balanced, -1 otherwise. - :param root: The root node of the binary tree. + :param root: Root node of the binary tree. :type root: binarytree.Node | None - :return: The height or -1. + :return: Height if the binary tree is balanced, -1 otherwise. :rtype: int """ if root is None: @@ -38,11 +39,11 @@ def _is_balanced(root): def _is_bst(root, min_value=float('-inf'), max_value=float('inf')): """Check if the binary tree is a BST (binary search tree). - :param root: The root node of the binary tree. + :param root: Root node of the binary tree. :type root: binarytree.Node | None - :param min_value: The minimum node value seen. + :param min_value: Minimum node value seen. :type min_value: int | float - :param max_value: The maximum node value seen. + :param max_value: Maximum node value seen. :type max_value: int | float :return: True if the binary tree is a BST, False otherwise. :rtype: bool @@ -59,23 +60,20 @@ def _is_bst(root, min_value=float('-inf'), max_value=float('inf')): def _validate_tree_height(height): """Check if the height of the binary tree is valid. - :param height: The height of the binary tree (must be 0 - 9 inclusive). + :param height: Height of the binary tree (must be 0 - 9 inclusive). :type height: int - :raise binarytree.exceptions.InvalidTreeHeightError: - If an invalid tree height is given. + :raise binarytree.exceptions.TreeHeightError: If height is invalid. """ if not (isinstance(height, int) and 0 <= height <= 9): - raise InvalidTreeHeightError( - 'The height must be an integer between 0 - 9' - ) + raise TreeHeightError('height must be an int between 0 - 9') def _generate_perfect_bst(height): - """Generate a perfect BST (binary search tree) and return its root node. + """Generate a perfect BST (binary search tree) and return its root. - :param height: The height of the binary tree to build. + :param height: Height of the BST. :type height: int - :return: The root node of the BST. + :return: Root node of the BST. :rtype: binarytree.Node """ max_node_count = 2 ** (height + 1) - 1 @@ -87,8 +85,8 @@ def _build_bst_from_sorted_values(sorted_values): """Recursively build a perfect BST from odd number of sorted values. :param sorted_values: Odd number of sorted values. - :type sorted_values: [int] - :return: The root node of the BST. + :type sorted_values: [int | float] + :return: Root node of the BST. :rtype: binarytree.Node """ if len(sorted_values) == 0: @@ -103,9 +101,9 @@ def _build_bst_from_sorted_values(sorted_values): def _generate_random_leaf_count(height): """Return a random leaf count for building binary trees. - :param height: The height of the binary tree to build. + :param height: Height of the binary tree. :type height: int - :return: Randomly generated leaf count. + :return: Random leaf count. :rtype: int """ max_leaf_count = 2 ** height @@ -120,7 +118,7 @@ def _generate_random_leaf_count(height): def _generate_random_node_values(height): """Return random node values for building binary trees. - :param height: The height of the binary tree to build. + :param height: Height of the binary tree. :type height: int :return: Randomly generated node values. :rtype: [int] @@ -132,33 +130,32 @@ def _generate_random_node_values(height): def _build_tree_string(root, curr_index, index=False, delimiter='-'): - """Recursively traverse down the binary tree build a pretty-print string. + """Recursively walk down the binary tree and build a pretty-print string. In each recursive call, a "box" of characters visually representing the current (sub)tree is constructed line by line. Each line is padded with whitespaces to ensure all lines in the box have the same length. Then the - box, its width, and start-end positions of its root value repr (required - for drawing branches) are sent up to the parent call. The parent call then - combines its left and right sub-boxes to construct a larger box etc. + box, its width, and start-end positions of its root node value repr string + (required for drawing branches) are sent up to the parent call. The parent + call then combines its left and right sub-boxes to build a larger box etc. - :param root: The root node of the binary tree. + :param root: Root node of the binary tree. :type root: binarytree.Node | None - :param curr_index: The level-order_ index of the current node (root is 0). + :param curr_index: Level-order_ index of the current node (root node is 0). :type curr_index: int - :param index: If set to True, include the level-order_ node indexes - using the following format: ``{index}{delimiter}{value}`` - (default: False). + :param index: If set to True, include the level-order_ node indexes using + the following format: ``{index}{delimiter}{value}`` (default: False). :type index: bool - :param delimiter: The delimiter character between the node index and value - (default: '-'). + :param delimiter: Delimiter character between the node index and the node + value (default: '-'). :type delimiter: - :return: The box of characters visually representing the current subtree, - the width of the box, and the start-end positions of the new root value - repr string. + :return: Box of characters visually representing the current subtree, width + of the box, and start-end positions of the repr string of the new root + node value. :rtype: ([str], int, int, int) - .. _level-order: - https://en.wikipedia.org/wiki/Tree_traversal + .. _Level-order: + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search """ if root is None: return [], 0, 0, 0 @@ -178,8 +175,8 @@ def _build_tree_string(root, curr_index, index=False, delimiter='-'): r_box, r_box_width, r_root_start, r_root_end = \ _build_tree_string(root.right, 2 * curr_index + 2, index, delimiter) - # Draw the branch connecting the current root to the left sub-box - # Pad with whitespaces where necessary + # Draw the branch connecting the current root node to the left sub-box + # Pad the line with whitespaces where necessary if l_box_width > 0: l_root = (l_root_start + l_root_end) // 2 + 1 line1.append(' ' * (l_root + 1)) @@ -191,12 +188,12 @@ def _build_tree_string(root, curr_index, index=False, delimiter='-'): else: new_root_start = 0 - # Draw the representation of the current root + # Draw the representation of the current root node line1.append(node_repr) line2.append(' ' * new_root_width) - # Draw the branch connecting the current root to the right sub-box - # Pad with whitespaces where necessary + # Draw the branch connecting the current root node to the right sub-box + # Pad the line with whitespaces where necessary if r_box_width > 0: r_root = (r_root_start + r_root_end) // 2 line1.append('_' * r_root) @@ -214,16 +211,16 @@ def _build_tree_string(root, curr_index, index=False, delimiter='-'): r_line = r_box[i] if i < len(r_box) else ' ' * r_box_width new_box.append(l_line + gap + r_line) - # Return the new box, its width and its root positions + # Return the new box, its width and its root repr positions return new_box, len(new_box[0]), new_root_start, new_root_end def _get_tree_properties(root): """Inspect the binary tree and return its properties (e.g. height). - :param root: The root node of the binary tree. + :param root: Root node of the binary tree. :rtype: binarytree.Node - :return: The properties of the binary tree. + :return: Binary tree properties. :rtype: dict """ is_descending = True @@ -249,7 +246,7 @@ def _get_tree_properties(root): min_node_value = min(value, min_node_value) max_node_value = max(value, max_node_value) - # The node is a leaf. + # Node is a leaf. if node.left is None and node.right is None: if min_leaf_depth == 0: min_leaf_depth = max_leaf_depth @@ -299,42 +296,39 @@ def _get_tree_properties(root): class Node(object): """Represents a binary tree node. - This class provides methods and properties for managing the calling node, - and the binary tree which the calling node is the root of. Whenever a - docstring in this class says "binary tree", it is referring to the calling - node instance and its descendants. + This class provides methods and properties for managing the current node + instance, and the binary tree in which the node is the root of. When a + docstring in this class mentions "binary tree", it is referring to the + current node and its descendants. - :param value: The node value. Only integers are supported. - :type value: int - :param left: The left child node (default: None). + :param value: Node value (must be a number). + :type value: int | float + :param left: Left child node (default: None). :type left: binarytree.Node | None - :param right: The right child node (default: None). + :param right: Right child node (default: None). :type right: binarytree.Node | None - :raise binarytree.exceptions.InvalidNodeValueError: - If the node value is not an integer. - :raise binarytree.exceptions.InvalidNodeTypeError: - If the left or right child is not an instance of - :class:`binarytree.Node`. + :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 a number + (e.g. int, float). """ def __init__(self, value, left=None, right=None): - if not isinstance(value, int): - raise InvalidNodeValueError('The node value must be an integer') + if not isinstance(value, numbers.Number): + raise NodeValueError('node value must be a number') if left is not None and not isinstance(left, Node): - raise InvalidNodeTypeError( - 'The left child node is not a binarytree.Node instance') + raise NodeTypeError('left child must be a Node instance') if right is not None and not isinstance(right, Node): - raise InvalidNodeTypeError( - 'The right child node is not a binarytree.Node instance') + raise NodeTypeError('right child must be a Node instance') self.value = value self.left = left self.right = right def __repr__(self): - """Return the string representation of the node. + """Return the string representation of the current node. - :return: The string representation. + :return: String representation. :rtype: str | unicode **Example**: @@ -351,7 +345,7 @@ def __repr__(self): def __str__(self): """Return the pretty-print string for the binary tree. - :return: The pretty-print string. + :return: Pretty-print string. :rtype: str | unicode **Example**: @@ -375,29 +369,28 @@ def __str__(self): .. note:: + To include level-order_ indexes in the output string, use + :func:`binarytree.Node.pprint` instead. - To include `level-order (breadth-first)`_ indexes in the string, - use :func:`binarytree.Node.pprint` instead. - - .. _level-order (breadth-first): + .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search """ lines = _build_tree_string(self, 0, False, '-')[0] return '\n' + '\n'.join((line.rstrip() for line in lines)) - def __setattr__(self, attribute, obj): - """Modified version of **__setattr__** with extra sanity checks - around class attributes **left**, **right** and **value**. + def __setattr__(self, attr, obj): + """Modified version of ``__setattr__`` with extra sanity checking. + + Class attributes **left**, **right** and **value** are validated. - :param attribute: The name of the class attribute. - :type attribute: str | unicode - :param obj: The object to set. + :param attr: Name of the class attribute. + :type attr: str | unicode + :param obj: Object to set. :type obj: object - :raise binarytree.exceptions.InvalidNodeTypeError: - If the left or right child is not an instance of - :class:`binarytree.Node`. - :raise binarytree.exceptions.InvalidNodeValueError: - If the node value is not an integer. + :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). **Example**: @@ -409,7 +402,7 @@ def __setattr__(self, attribute, obj): >>> node.left = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - InvalidNodeTypeError: The node is not a binarytree.Node instance + NodeTypeError: Left child must be a Node instance .. doctest:: @@ -419,28 +412,29 @@ def __setattr__(self, attribute, obj): >>> node.value = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - InvalidNodeValueError: The node value must be an integer + NodeValueError: node value must be a number """ - if attribute == 'left' or attribute == 'right': + if attr == 'left': + if obj is not None and not isinstance(obj, Node): + raise NodeTypeError( + 'left child must be a Node instance') + elif attr == 'right': if obj is not None and not isinstance(obj, Node): - raise InvalidNodeTypeError( - 'The node is not a binarytree.Node instance') - elif attribute == 'value' and not isinstance(obj, int): - raise InvalidNodeValueError('The node value must be an integer') - object.__setattr__(self, attribute, obj) + raise NodeTypeError( + 'right child must be a Node instance') + elif attr == 'value' and not isinstance(obj, numbers.Number): + raise NodeValueError('node value must be a number') + + object.__setattr__(self, attr, obj) def __iter__(self): - """Return the `list representation`_ of the binary tree. + """Iterate through the nodes in the binary tree in level-order_. - .. _list representation: - https://en.wikipedia.org/wiki/Binary_tree#Arrays + .. _level-order: + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search - :return: The list representation consisting of node values or None's. - If a node has an index i, its left child is at index 2i + 1, right - child at index 2i + 2, and parent at index floor((i - 1) / 2). None - signifies the absence of a node. See example below for an - illustration. - :rtype: [int | None] + :return: Node iterator. + :rtype: (binarytree.Node) **Example**: @@ -451,42 +445,36 @@ def __iter__(self): >>> root = Node(1) >>> root.left = Node(2) >>> root.right = Node(3) - >>> root.left.right = Node(4) + >>> root.left.left = Node(4) + >>> root.left.right = Node(5) >>> - >>> list(root) - [1, 2, 3, None, 4] + >>> print(root) + + __1 + / \\ + 2 3 + / \\ + 4 5 + + >>> [node for node in root] + [Node(1), Node(2), Node(3), Node(4), Node(5)] """ current_nodes = [self] - has_more_nodes = True - values = [] - while has_more_nodes: - has_more_nodes = False + while len(current_nodes) > 0: next_nodes = [] for node in current_nodes: - if node is None: - values.append(None) - next_nodes.extend((None, None)) - continue - - if node.left is not None or node.right is not None: - has_more_nodes = True - - values.append(node.value) - next_nodes.extend((node.left, node.right)) - + yield node + if node.left is not None: + next_nodes.append(node.left) + if node.right is not None: + next_nodes.append(node.right) current_nodes = next_nodes - # Get rid of the trailing None entries - while values and values[-1] is None: - values.pop() - - return iter(values) - def __len__(self): """Return the total number of nodes in the binary tree. - :return: The total number of nodes. + :return: Total number of nodes. :rtype: int **Example**: @@ -503,26 +491,22 @@ def __len__(self): 3 .. note:: - This method is equivalent to :attr:`binarytree.Node.size`. """ return self.properties['size'] def __getitem__(self, index): - """Return the node/subtree at the give `level-order (breadth-first)`_ - index. + """Return the node (or subtree) at the given level-order_ index. - :param index: The node index. + .. _level-order: + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + :param index: Level-order index of the node. :type index: int - :return: The node at the given index. + :return: Node (or subtree) at the given index. :rtype: binarytree.Node - :raise binarytree.exceptions.InvalidNodeIndexError: - If an invalid index is given. - :raise binarytree.exceptions.NodeNotFoundError: - If the target node does not exist. - - .. _level-order (breadth-first): - https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + :raise binarytree.exceptions.NodeIndexError: If node index is invalid. + :raise binarytree.exceptions.NodeNotFoundError: If the node is missing. **Example**: @@ -543,11 +527,11 @@ def __getitem__(self, index): >>> root[3] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: Node missing at index 3 + NodeNotFoundError: node missing at index 3 """ if not isinstance(index, int) or index < 0: - raise InvalidNodeIndexError( - 'The node index must be a non-negative integer') + raise NodeIndexError( + 'node index must be a non-negative int') current_nodes = [self] current_index = 0 @@ -574,29 +558,27 @@ def __getitem__(self, index): current_nodes = next_nodes - raise NodeNotFoundError('Node missing at index {}'.format(index)) + raise NodeNotFoundError('node missing at index {}'.format(index)) def __setitem__(self, index, node): - """Insert the node/subtree into the binary tree at the given - `level-order (breadth-first)`_ index. + """Insert a node (or subtree) at the given level-order_ index. - * An exception is raised if the parent node does not exist. - * Any existing node/subtree is overwritten. - * The root node (calling node) cannot be replaced. + * An exception is raised if the parent node is missing. + * Any existing node or subtree is overwritten. + * Root node (current node) cannot be replaced. - :param index: The node index. + .. _level-order: + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + + :param index: Level-order index of the node. :type index: int - :param node: The new node to insert. + :param node: Node to insert. :type node: binarytree.Node - :raise binarytree.exceptions.OperationForbiddenError: - If the user tries to overwrite the root node (calling node). - :raise binarytree.exceptions.NodeNotFoundError: - If the parent for the new node does not exist. - :raise binarytree.exceptions.InvalidNodeTypeError: - If the new node is not an instance of :class:`binarytree.Node`. - - .. _level-order (breadth-first): - https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + :raise binarytree.exceptions.NodeTypeError: If new node is not an + instance of :class:`binarytree.Node`. + :raise binarytree.exceptions.NodeNotFoundError: If parent is missing. + :raise binarytree.exceptions.NodeModifyError: If user attempts to + overwrite the root node (current node). **Example**: @@ -611,7 +593,7 @@ def __setitem__(self, index, node): >>> root[0] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - OperationForbiddenError: Cannot modify the root node + NodeModifyError: cannot modify the root node .. doctest:: @@ -624,7 +606,7 @@ def __setitem__(self, index, node): >>> root[11] = Node(4) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: Parent node missing at index 5 + NodeNotFoundError: parent node missing at index 5 .. doctest:: @@ -640,34 +622,34 @@ def __setitem__(self, index, node): Node(4) """ if index == 0: - raise OperationForbiddenError('Cannot modify the root node') + raise NodeModifyError('cannot modify the root node') parent_index = (index - 1) // 2 try: parent = self.__getitem__(parent_index) except NodeNotFoundError: raise NodeNotFoundError( - 'Parent node missing at index {}'.format(parent_index)) + 'parent node missing at index {}'.format(parent_index)) + setattr(parent, 'left' if index % 2 else 'right', node) def __delitem__(self, index): - """Remove the node/subtree at the given `level-order (breadth-first)`_ - index from the binary tree. + """Remove the node (or subtree) at the given level-order_ index. - * An exception is raised if the target node does not exist. + * An exception is raised if the target node is missing. * The descendants of the target node (if any) are also removed. - * The root node (calling node) cannot be deleted. + * Root node (current node) cannot be deleted. - :param index: The node index. - :type index: int - :raise binarytree.exceptions.OperationForbiddenError: - If the user tries to delete the root node (calling node). - :raise binarytree.exceptions.NodeNotFoundError: - If the target node or its parent does not exist. - - .. _level-order (breadth-first): + .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search + :param index: Level-order index of the node. + :type index: int + :raise binarytree.exceptions.NodeNotFoundError: If the target node or + its parent is missing. + :raise binarytree.exceptions.NodeModifyError: If user attempts to + delete the root node (current node). + **Example**: .. doctest:: @@ -681,7 +663,7 @@ def __delitem__(self, index): >>> del root[0] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - OperationForbiddenError: Cannot delete the root node + NodeModifyError: cannot delete the root node .. doctest:: @@ -696,33 +678,33 @@ def __delitem__(self, index): >>> root[2] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: Node missing at index 2 + NodeNotFoundError: node missing at index 2 """ if index == 0: - raise OperationForbiddenError('Cannot delete the root node') + raise NodeModifyError('cannot delete the root node') parent_index = (index - 1) // 2 try: parent = self.__getitem__(parent_index) except NodeNotFoundError: raise NodeNotFoundError( - 'No node to delete at index {}'.format(index)) + 'no node to delete at index {}'.format(index)) child_attr = 'left' if index % 2 == 1 else 'right' if getattr(parent, child_attr) is None: raise NodeNotFoundError( - 'No node to delete at index {}'.format(index)) + 'no node to delete at index {}'.format(index)) + setattr(parent, child_attr, None) def pprint(self, index=False, delimiter='-'): """Pretty-print the binary tree. - :param index: If set to True (default: False), display the - `level-order (breadth-first)`_ indexes using the following - format: "{index}{delimiter}{value}". + :param index: If set to True (default: False), display level-order_ + indexes using the format: ``{index}{delimiter}{value}``. :type index: bool - :param delimiter: The delimiter character between the node index, and - the node value (default: "-"). + :param delimiter: Delimiter character between the node index and + the node value (default: '-'). :type delimiter: str | unicode **Example**: @@ -744,7 +726,7 @@ def pprint(self, index=False, delimiter='-'): \\ 4 - >>> root.pprint(index=True) # Format: {index}-{value} + >>> root.pprint(index=True) # Format: {index}-{value} _____0-1_ / \\ @@ -754,8 +736,11 @@ def pprint(self, index=False, delimiter='-'): .. note:: - If you don't need to see the node indexes, you can use - :func:`binarytree.Node.__str__`. + If you do not need level-order_ indexes in the output string, use + :func:`binarytree.Node.__str__` instead. + + .. _level-order: + https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search """ lines = _build_tree_string(self, 0, index, delimiter)[0] print('\n' + '\n'.join((line.rstrip() for line in lines))) @@ -763,12 +748,12 @@ def pprint(self, index=False, delimiter='-'): def validate(self): """Check if the binary tree is malformed. - :raise binarytree.exceptions.CyclicNodeReferenceError: - If there is a cyclic reference to a node in the binary tree. - :raise binarytree.exceptions.InvalidNodeTypeError: - If a node is not an instance of :class:`binarytree.Node`. - :raise binarytree.exceptions.InvalidNodeValueError: - If a node value is not an integer. + :raise binarytree.exceptions.NodeReferenceError: If there is a + 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). **Example**: @@ -783,49 +768,101 @@ def validate(self): >>> root.validate() # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - CyclicNodeReferenceError: Cyclic node reference at index 0 + NodeReferenceError: cyclic node reference at index 0 """ has_more_nodes = True - nodes_visited = set() - current_nodes = [self] - current_index = 0 + visited = set() + to_visit = [self] + index = 0 while has_more_nodes: has_more_nodes = False next_nodes = [] - for node in current_nodes: + for node in to_visit: if node is None: next_nodes.extend((None, None)) else: - if node in nodes_visited: - raise CyclicNodeReferenceError( - 'Cyclic node reference at index {}' - .format(current_index) - ) + if node in visited: + raise NodeReferenceError( + 'cyclic node reference at index {}'.format(index)) if not isinstance(node, Node): - raise InvalidNodeTypeError( - 'Invalid node instance at index {}' - .format(current_index) - ) - if not isinstance(node.value, int): - raise InvalidNodeValueError( - 'Invalid node value at index {}' - .format(current_index) - ) + raise NodeTypeError( + 'invalid node instance at index {}'.format(index)) + if not isinstance(node.value, numbers.Number): + raise NodeValueError( + 'invalid node value at index {}'.format(index)) if node.left is not None or node.right is not None: has_more_nodes = True - nodes_visited.add(node) + visited.add(node) next_nodes.extend((node.left, node.right)) - current_index += 1 + index += 1 + + to_visit = next_nodes + + @property + def values(self): + """Return the `list representation`_ of the binary tree. + + .. _list representation: + https://en.wikipedia.org/wiki/Binary_tree#Arrays + + :return: List representation of the binary tree, which is a list of + node values in breadth-first order starting from the root (current + node). If a node is at index i, its left child is always at 2i + 1, + right child at 2i + 2, and parent at index floor((i - 1) / 2). None + indicates absence of a node at that index. See example below for an + illustration. + :rtype: [int | float | None] + + **Example**: + + .. doctest:: + + >>> from binarytree import Node + >>> + >>> root = Node(1) + >>> root.left = Node(2) + >>> root.right = Node(3) + >>> root.left.right = Node(4) + >>> + >>> root.values + [1, 2, 3, None, 4] + """ + current_nodes = [self] + has_more_nodes = True + values = [] + + while has_more_nodes: + has_more_nodes = False + next_nodes = [] + for node in current_nodes: + if node is None: + values.append(None) + next_nodes.extend((None, None)) + continue + + if node.left is not None or node.right is not None: + has_more_nodes = True + + values.append(node.value) + next_nodes.extend((node.left, node.right)) current_nodes = next_nodes + # Get rid of trailing None's + while values and values[-1] is None: + values.pop() + + return values + @property def leaves(self): - """Return the leaves of the binary tree. + """Return the leaf nodes of the binary tree. - :return: The list of leaf nodes. + A leaf node is any node that does not have child nodes. + + :return: List of leaf nodes. :rtype: [binarytree.Node] **Example**: @@ -870,7 +907,7 @@ def leaves(self): def levels(self): """Return the nodes in the binary tree level by level. - :return: The per-level lists of nodes. + :return: Lists of nodes level by level. :rtype: [[binarytree.Node]] **Example**: @@ -914,7 +951,11 @@ def levels(self): def height(self): """Return the height of the binary tree. - :return: The height of the binary tree. + Height of a binary tree is the number of edges on the longest path + between the root node and a leaf node. Binary tree with just a single + node has a height of 0. + + :return: Height of the binary tree. :rtype: int **Example**: @@ -939,7 +980,6 @@ def height(self): 2 .. note:: - A binary tree with only a root node has a height of 0. """ return _get_tree_properties(self)['height'] @@ -948,7 +988,7 @@ def height(self): def size(self): """Return the total number of nodes in the binary tree. - :return: The total number of nodes. + :return: Total number of nodes. :rtype: int **Example**: @@ -966,16 +1006,17 @@ def size(self): 4 .. note:: - This method is equivalent to :func:`binarytree.Node.__len__`. """ return _get_tree_properties(self)['size'] @property def leaf_count(self): - """Return the total number of leaves in the binary tree. + """Return the total number of leaf nodes in the binary tree. + + A leaf node is a node with no child nodes. - :return: The total number of leaves. + :return: Total number of leaf nodes. :rtype: int **Example**: @@ -996,7 +1037,15 @@ def leaf_count(self): @property def is_balanced(self): - """Return True if the binary tree is height-balanced, False otherwise. + """Check if the binary tree is height-balanced. + + A binary tree is height-balanced if it meets the following criteria: + + * Left subtree is height-balanced. + * Right subtree is height-balanced. + * The difference between heights of left and right subtrees is no more + than 1. + * An empty binary tree is always height-balanced. :return: True if the binary tree is balanced, False otherwise. :rtype: bool @@ -1026,12 +1075,13 @@ def is_balanced(self): @property def is_bst(self): - """Return True if the binary tree is a BST (binary search tree), - False otherwise. + """Check if the binary tree is a BST_ (binary search tree). - :return: True if the binary tree is a BST, False otherwise. + :return: True if the binary tree is a BST_, False otherwise. :rtype: bool + .. _BST: https://en.wikipedia.org/wiki/Binary_search_tree + **Example**: .. doctest:: @@ -1055,11 +1105,13 @@ def is_bst(self): @property def is_max_heap(self): - """Return True if the binary tree is a max heap, False otherwise. + """Check if the binary tree is a `max heap`_. - :return: True if the binary tree is a max heap, False otherwise. + :return: True if the binary tree is a `max heap`_, False otherwise. :rtype: bool + .. _max heap: https://en.wikipedia.org/wiki/Min-max_heap + **Example**: .. doctest:: @@ -1083,11 +1135,13 @@ def is_max_heap(self): @property def is_min_heap(self): - """Return True if the binary tree is a min heap, False otherwise. + """Check if the binary tree is a `min heap`_. - :return: True if the binary tree is a min heap, False otherwise. + :return: True if the binary tree is a `min heap`_, False otherwise. :rtype: bool + .. _min heap: https://en.wikipedia.org/wiki/Min-max_heap + **Example**: .. doctest:: @@ -1111,8 +1165,10 @@ def is_min_heap(self): @property def is_perfect(self): - """Return True if the binary tree is perfect (i.e. all levels are - completely filled), False otherwise. + """Check if the binary tree is perfect. + + A binary tree is perfect if all its levels are completely filled. See + example below for an illustration. :return: True if the binary tree is perfect, False otherwise. :rtype: bool @@ -1146,8 +1202,10 @@ def is_perfect(self): @property def is_strict(self): - """Return True if the binary tree is strict (i.e. all non-leaf nodes - have both children), False otherwise. + """Check if the binary tree is strict. + + A binary tree is strict if all its non-leaf nodes have both the left + and right child nodes. :return: True if the binary tree is strict, False otherwise. :rtype: bool @@ -1174,18 +1232,17 @@ def is_strict(self): >>> root.is_strict True - - .. note:: - - Strictly binary nodes are also called **full** nodes. """ return _get_tree_properties(self)['is_strict'] @property def is_complete(self): - """Return True if the binary tree is complete (i.e. all levels except - possibly the last are completely filled, and the last level is always - left-justified), False otherwise. + """Check if the binary tree is complete. + + A binary tree is complete if it meets the following criteria: + + * All levels except possibly the last are completely filled. + * Last level is left-justified. :return: True if the binary tree is complete, False otherwise. :rtype: bool @@ -1217,9 +1274,9 @@ def is_complete(self): @property def min_node_value(self): - """Return the minimum node value in the binary tree. + """Return the minimum node value of the binary tree. - :return: The minimum node value. + :return: Minimum node value. :rtype: int **Example**: @@ -1239,9 +1296,9 @@ def min_node_value(self): @property def max_node_value(self): - """Return the maximum node value in the binary tree. + """Return the maximum node value of the binary tree. - :return: The maximum node value. + :return: Maximum node value. :rtype: int **Example**: @@ -1261,9 +1318,9 @@ def max_node_value(self): @property def max_leaf_depth(self): - """Return the maximum leaf node depth in the binary tree. + """Return the maximum leaf node depth of the binary tree. - :return: The maximum leaf node depth. + :return: Maximum leaf node depth. :rtype: int **Example**: @@ -1295,9 +1352,9 @@ def max_leaf_depth(self): @property def min_leaf_depth(self): - """Return the minimum leaf node depth in the binary tree. + """Return the minimum leaf node depth of the binary tree. - :return: The minimum leaf node depth. + :return: Minimum leaf node depth. :rtype: int **Example**: @@ -1329,9 +1386,9 @@ def min_leaf_depth(self): @property def properties(self): - """Return various properties of the the binary tree all at once. + """Return various properties of the binary tree. - :return: The properties of the binary tree. + :return: Binary tree properties. :rtype: dict **Example**: @@ -1385,13 +1442,13 @@ def properties(self): @property def inorder(self): - """Return the nodes in the binary tree using in-order_ (left, root, - right) traversal. + """Return the nodes in the binary tree using in-order_ traversal. - .. _in-order: - https://en.wikipedia.org/wiki/Tree_traversal + An in-order_ traversal visits left subtree, root, then right subtree. - :return: The list of nodes. + .. _in-order: https://en.wikipedia.org/wiki/Tree_traversal + + :return: List of nodes. :rtype: [binarytree.Node] **Example**: @@ -1436,13 +1493,13 @@ def inorder(self): @property def preorder(self): - """Return the nodes in the binary tree using pre-order_ (root, left, - right) traversal. + """Return the nodes in the binary tree using pre-order_ traversal. + + A pre-order_ traversal visits root, left subtree, then right subtree. - .. _pre-order: - https://en.wikipedia.org/wiki/Tree_traversal + .. _pre-order: https://en.wikipedia.org/wiki/Tree_traversal - :return: The list of nodes. + :return: List of nodes. :rtype: [binarytree.Node] **Example**: @@ -1468,31 +1525,30 @@ def preorder(self): >>> root.preorder [Node(1), Node(2), Node(4), Node(5), Node(3)] """ - node_values = [] node_stack = [self] + result = [] while len(node_stack) > 0: node = node_stack.pop() - node_values.append(node) + result.append(node) if node.right is not None: node_stack.append(node.right) if node.left is not None: node_stack.append(node.left) - return node_values + return result @property def postorder(self): - """Return the nodes in the binary tree using post-order_ (left, right, - root) traversal. + """Return the nodes in the binary tree using post-order_ traversal. - .. _post-order: - https://en.wikipedia.org/wiki/Tree_traversal + A post-order_ traversal visits left subtree, right subtree, then root. - :return: The list of nodes. - :rtype: [binarytree.Node] + .. _post-order: https://en.wikipedia.org/wiki/Tree_traversal + :return: List of nodes. + :rtype: [binarytree.Node] **Example**: @@ -1517,8 +1573,8 @@ def postorder(self): >>> root.postorder [Node(4), Node(5), Node(2), Node(3), Node(1)] """ - node_values = [] node_stack = [] + result = [] node = self while True: @@ -1536,23 +1592,24 @@ def postorder(self): node_stack.append(node) node = node.right else: - node_values.append(node) + result.append(node) node = None if len(node_stack) == 0: break - return node_values + return result @property def levelorder(self): - """Return the nodes in the binary tree using - `level-order (breadth-first)`_ traversal. + """Return the nodes in the binary tree using level-order_ traversal. - .. _level-order (breadth-first): + A level-order_ traversal visits nodes left to right, level by level. + + .. _level-order: https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search - :return: The list of nodes. + :return: List of nodes. :rtype: [binarytree.Node] **Example**: @@ -1579,39 +1636,38 @@ def levelorder(self): [Node(1), Node(2), Node(3), Node(4), Node(5)] """ current_nodes = [self] - node_values = [] + result = [] while len(current_nodes) > 0: next_nodes = [] for node in current_nodes: - node_values.append(node) + result.append(node) if node.left is not None: next_nodes.append(node.left) if node.right is not None: next_nodes.append(node.right) current_nodes = next_nodes - return node_values + return result def build(values): - """Build a binary tree from a `list representation`_ (i.e. a list of - node values and/or None's in breadth-first order) and return its root. - - :param values: The list representation (i.e. a list of node values and/or - None's in breadth-first order). If a node has an index i, its left - child is at index 2i + 1, right child at index 2i + 2, and parent at - index floor((i - 1) / 2). None signifies the absence of a node. See - example below for an illustration. - :type values: [int | None] - :return: The root of the binary tree. - :rtype: binarytree.Node - :raise binarytree.exceptions.NodeNotFoundError: - If the list representation is malformed and a parent node is missing. + """Build a tree from `list representation`_ and return its root node. .. _list representation: https://en.wikipedia.org/wiki/Binary_tree#Arrays + :param values: List representation of the binary tree, which is a list of + node values in breadth-first order starting from the root (current + 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] + :return: Root node of the binary tree. + :rtype: binarytree.Node + :raise binarytree.exceptions.NodeNotFoundError: If the list representation + is malformed (e.g. a parent node is missing). + **Example**: .. doctest:: @@ -1636,7 +1692,7 @@ def build(values): >>> root = build([None, 2, 3]) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeNotFoundError: Parent node missing at index 0 + NodeNotFoundError: parent node missing at index 0 """ nodes = [None if v is None else Node(v) for v in values] @@ -1647,9 +1703,7 @@ def build(values): parent = nodes[parent_index] if parent is None: raise NodeNotFoundError( - 'Parent node missing at index {}' - .format(parent_index) - ) + 'parent node missing at index {}'.format(parent_index)) setattr(parent, 'left' if index % 2 else 'right', node) return nodes[0] if nodes else None @@ -1658,16 +1712,15 @@ def build(values): def tree(height=3, is_perfect=False): """Generate a random binary tree and return its root node. - :param height: The height of the tree (default: 3, range: 0 - 9 inclusive). + :param height: Height of the tree (default: 3, range: 0 - 9 inclusive). :type height: int :param is_perfect: If set to True (default: False), a perfect binary tree - with all levels filled is returned. When set to False, a perfect binary - tree may still be generated and returned by chance. + with all levels filled is returned. If set to False, a perfect binary + tree may still be generated by chance. :type is_perfect: bool - :return: The root node of the generated tree. + :return: Root node of the binary tree. :rtype: binarytree.Node - :raise binarytree.exceptions.InvalidTreeHeightError: - If an invalid tree height is given. + :raise binarytree.exceptions.TreeHeightError: If height is invalid. **Example**: @@ -1698,7 +1751,7 @@ def tree(height=3, is_perfect=False): >>> root = tree(height=20) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - InvalidTreeHeightError: The height must be an integer between 0 - 9 + TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) values = _generate_random_node_values(height) @@ -1733,16 +1786,15 @@ def tree(height=3, is_perfect=False): def bst(height=3, is_perfect=False): """Generate a random BST (binary search tree) and return its root node. - :param height: The height of the BST (default: 3, range: 0 - 9 inclusive). + :param height: Height of the BST (default: 3, range: 0 - 9 inclusive). :type height: int :param is_perfect: If set to True (default: False), a perfect BST with all - levels filled is returned. When set to False, a perfect BST may still - be generated and returned by chance. + levels filled is returned. If set to False, a perfect BST may still be + generated by chance. :type is_perfect: bool - :return: The root node of the generated BST. + :return: Root node of the BST. :rtype: binarytree.Node - :raise binarytree.exceptions.InvalidTreeHeightError: - If an invalid tree height is given. + :raise binarytree.exceptions.TreeHeightError: If height is invalid. **Example**: @@ -1764,7 +1816,7 @@ def bst(height=3, is_perfect=False): >>> root = bst(10) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - InvalidTreeHeightError: The height must be an integer between 0 - 9 + TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) if is_perfect: @@ -1798,22 +1850,21 @@ def bst(height=3, is_perfect=False): def heap(height=3, is_max=True, is_perfect=False): - """Generate a heap and return its root node. + """Generate a random heap and return its root node. - :param height: The height of the heap (default: 3, range: 0 - 9 inclusive). + :param height: Height of the heap (default: 3, range: 0 - 9 inclusive). :type height: int - :param is_max: If set to True (default: True), generate a max heap. - Otherwise, generate a min heap. Note that a binary tree with only the - root is both a min and max heap. + :param is_max: If set to True (default: True), generate a max heap. If set + to False, generate a min heap. A binary tree with only the root node is + considered both a min and max heap. :type is_max: bool :param is_perfect: If set to True (default: False), a perfect heap with all - levels filled is returned. When set to False, a perfect heap may still - be generated and returned by chance. + levels filled is returned. If set to False, a perfect heap may still be + generated by chance. :type is_perfect: bool - :return: The root node of the generated heap. + :return: Root node of the heap. :rtype: binarytree.Node - :raise binarytree.exceptions.InvalidTreeHeightError: - If an invalid tree height is given. + :raise binarytree.exceptions.TreeHeightError: If height is invalid. **Example**: @@ -1859,7 +1910,7 @@ def heap(height=3, is_max=True, is_perfect=False): >>> root = heap(-1) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - InvalidTreeHeightError: The height must be an integer between 0 - 9 + TreeHeightError: height must be an int between 0 - 9 """ _validate_tree_height(height) values = _generate_random_node_values(height) diff --git a/binarytree/exceptions.py b/binarytree/exceptions.py index 6f81cf3..6478d4e 100644 --- a/binarytree/exceptions.py +++ b/binarytree/exceptions.py @@ -2,32 +2,32 @@ class BinaryTreeError(Exception): - """Base exception.""" + """Base (catch-all) binarytree exception.""" -class InvalidNodeValueError(BinaryTreeError): - """Raised if a node has an invalid value.""" +class NodeIndexError(BinaryTreeError): + """Node index was invalid.""" -class InvalidNodeIndexError(BinaryTreeError): - """Raised if an invalid level-order index is given.""" +class NodeModifyError(BinaryTreeError): + """User tried to overwrite or delete the root node.""" -class InvalidNodeTypeError(BinaryTreeError): - """Raised if a node is not an instance of :class:`binarytree.Node`.""" +class NodeNotFoundError(BinaryTreeError): + """Node was missing from the binary tree.""" -class OperationForbiddenError(BinaryTreeError): - """Raised if the user tries to overwrite or delete the root node.""" +class NodeReferenceError(BinaryTreeError): + """Node reference was invalid (e.g. cyclic reference).""" -class NodeNotFoundError(BinaryTreeError): - """Raised if a node is missing from the binary tree.""" +class NodeTypeError(BinaryTreeError): + """Node was not an instance of :class:`binarytree.Node`.""" -class InvalidTreeHeightError(BinaryTreeError): - """Raised if an invalid tree height is given.""" +class NodeValueError(BinaryTreeError): + """Node value was not a number (e.g. int, float).""" -class CyclicNodeReferenceError(BinaryTreeError): - """Raised if the binary tree has a cyclic reference to a node.""" +class TreeHeightError(BinaryTreeError): + """Tree height was invalid.""" diff --git a/binarytree/version.py b/binarytree/version.py index 0f668da..8f48aa6 100644 --- a/binarytree/version.py +++ b/binarytree/version.py @@ -1 +1 @@ -__version__ = '3.0.1' # pragma: no cover \ No newline at end of file +__version__ = '4.0.0' # pragma: no cover diff --git a/docs/conf.py b/docs/conf.py index dd81138..707dfc6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -from binarytree.version import __version__ + +_version = {} +with open("../binarytree/version.py") as fp: + exec(fp.read(), _version) # -- General configuration ------------------------------------------------ @@ -28,9 +31,13 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.autosectionlabel'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages' +] # Add any paths that contain templates here, relative to this directory. templates_path = [] @@ -54,9 +61,9 @@ # built documents. # # The short X.Y version. -version = __version__ +version = _version['__version__'] # The full version, including alpha/beta/rc tags. -release = __version__ +release = _version['__version__'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/contributing.rst b/docs/contributing.rst index fb359da..032c57c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -5,31 +5,30 @@ Instructions ============ Before submitting a pull request on GitHub_, please make sure you meet the -following requirements: +following **requirements**: * The pull request points to the dev_ (development) branch. -* All changes are squashed into a single commit (I like to use - ``git rebase -i`` to do this). -* The commit message is in present tense (ok: "Add feature", not ok: +* All changes are squashed into a single commit (I like to use ``git rebase -i`` + to do this). +* The commit message is in present tense (good: "Add feature", bad: "Added feature"). -* Correct and consistent style: Sphinx_-compatible docstrings, using snake - vs. camel casing properly_ and PEP8_ compliance (see below). +* Correct and consistent style: Sphinx_-compatible docstrings, correct snake + and camel casing, and PEP8_ compliance (see below). * No classes/methods/functions with missing docstrings or commented-out lines. - You can refer to existing ones for examples. -* The test coverage_ remains at %100. Sometimes you may find yourself having to - write superfluous unit tests to keep this number up. If a piece of code is - trivial and has no need for unittests, use this_ to exclude it from coverage. + You can take a look at the existing code in binarytree for examples. +* The test coverage_ remains at %100. You may find yourself having to write + superfluous unit tests to keep this number up. If a piece of code is trivial + and has no need for tests, use this_ to exclude it from coverage. * No build failures on TravisCI_. The builds automatically trigger on PR submissions. * Does not break backward-compatibility (unless there is a really good reason). -* Compatible with Python versions 2.7, 3.4, 3.5 and 3.6. +* Compatibility with all supported Python versions: 2.7, 3.4, 3.5 and 3.6. .. warning:: The dev branch is occasionally rebased_, and its commit history may be - overwritten in the process (I try very hard never to do this). So before - you begin your feature work, git fetch/pull to ensure that branches have - not diverged. If you see git conflicts and just want to start from scratch, - run this command: + overwritten in the process. Before you begin feature work, git fetch or + pull to ensure that your local branch has not diverged. If you see git + conflicts and just want to start from scratch, run these commands: .. code-block:: bash @@ -49,15 +48,15 @@ To ensure PEP8_ compliance, run flake8_: ~$ cd binarytree ~$ flake8 -You should try to resolve all issues reported. If there is a good reason to -ignore errors from a specific piece of code, visit here_ to see how to exclude -the lines from the check. +You must resolve all issues reported. If there is a good reason to ignore +errors coming from a specific piece of code, visit here_ to see how to exclude +the lines. Testing ======= To test your changes, run the unit tests that come with **binarytree** on your -local machine. The tests are written using pytest_. +local machine. The tests use pytest_. To run the unit tests: @@ -66,7 +65,7 @@ To run the unit tests: ~$ pip install pytest ~$ git clone https://github.com/joowani/binarytree.git ~$ cd binarytree - ~$ py.test tests.py --verbose + ~$ py.test --verbose To run the unit tests with coverage report: @@ -75,9 +74,9 @@ To run the unit tests with coverage report: ~$ pip install coverage pytest pytest-cov ~$ git clone https://github.com/joowani/binarytree.git ~$ cd binarytree - ~$ py.test tests.py --verbose --cov-report=html --cov=binarytree - ~$ # Open the generated file htmlcov/index.html in a browser + ~$ py.test --verbose --cov=binarytree --cov-report=html + # Open the generated file htmlcov/index.html in a browser Documentation ============= @@ -92,15 +91,14 @@ machine: ~$ git clone https://github.com/joowani/binarytree.git ~$ cd binarytree/docs ~$ sphinx-build . build - ~$ # Open the generated file build/index.html in a browser + # Open the generated file build/index.html in a browser -As always, thanks for your contribution! +As always, thank you for your contribution! .. _rebased: https://git-scm.com/book/en/v2/Git-Branching-Rebasing .. _dev: https://github.com/joowani/binarytree/tree/dev .. _GitHub: https://github.com/joowani/binarytree -.. _properly: https://stackoverflow.com/questions/159720 .. _PEP8: https://www.python.org/dev/peps/pep-0008/ .. _coverage: https://coveralls.io/github/joowani/binarytree .. _this: http://coverage.readthedocs.io/en/latest/excluding.html diff --git a/docs/exceptions.rst b/docs/exceptions.rst index e02584d..cbe7884 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -1,7 +1,7 @@ Exceptions ---------- -Below is the list of exceptions raised by **binarytree**: +The page lists the exceptions raised by **binarytree**: .. automodule:: binarytree.exceptions - :members: \ No newline at end of file + :members: diff --git a/docs/index.rst b/docs/index.rst index 38600fc..8647171 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,16 +6,12 @@ Welcome to the documentation for **binarytree**! **Binarytree** is a Python library which provides a simple API to generate, visualize, inspect and manipulate binary trees. It allows you to 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. +algorithms. Heaps and BSTs (binary search trees) are also supported. Requirements ============ - Python 2.7, 3.4, 3.5 or 3.6 -- Pip_ installer - -.. _Pip: https://pip.pypa.io - Installation ============ @@ -38,7 +34,6 @@ You may need to use ``sudo`` depending on your environment. .. _PyPi: https://pypi.python.org/pypi/binarytree .. _GitHub: https://github.com/joowani/binarytree - Contents ======== @@ -46,6 +41,6 @@ Contents :maxdepth: 1 overview - api + specs exceptions contributing diff --git a/docs/overview.rst b/docs/overview.rst index 002ba63..8184920 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -3,7 +3,7 @@ Overview By default, **binarytree** uses the following class to represent a node: -.. code-block:: python +.. testcode:: class Node(object): @@ -12,7 +12,6 @@ By default, **binarytree** uses the following class to represent a node: self.left = left # Left child self.right = right # Right child - Generate and pretty-print various types of binary trees: .. code-block:: python @@ -60,10 +59,9 @@ Generate and pretty-print various types of binary trees: 0 10 6 -Use the :ref:`binarytree.Node ` class to build your -own trees: +Use the :class:`binarytree.Node` class to build your own trees: -.. code-block:: python +.. doctest:: >>> from binarytree import Node >>> @@ -73,17 +71,17 @@ own trees: >>> root.left.right = Node(4) >>> >>> print(root) - + __1 / \ 2 3 \ 4 - + Inspect tree properties: -.. code-block:: python +.. doctest:: >>> from binarytree import Node >>> @@ -94,13 +92,13 @@ Inspect tree properties: >>> root.left.right = Node(5) >>> >>> print(root) - + __1 / \ 2 3 / \ 4 5 - + >>> root.height 2 >>> root.is_balanced @@ -130,21 +128,13 @@ Inspect tree properties: >>> root.size 5 - >>> root.properties - {'height': 2, - 'is_balanced': True, - 'is_bst': False, - 'is_complete': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_perfect': False, - 'is_strict': True, - 'leaf_count': 3, - 'max_leaf_depth': 2, - 'max_node_value': 5, - 'min_leaf_depth': 1, - 'min_node_value': 1, - 'size': 5} + >>> properties = root.properties # Get all properties at once + >>> properties['height'] + 2 + >>> properties['is_balanced'] + True + >>> properties['max_leaf_depth'] + 2 >>> root.leaves [Node(3), Node(4), Node(5)] @@ -157,7 +147,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: .. _level-order (breadth-first): https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search -.. code-block:: python +.. doctest:: >>> from binarytree import Node >>> @@ -168,7 +158,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: >>> root.left.right.left = Node(5) # index: 9, value: 5 >>> >>> print(root) - + ____1 / \ 2__ 3 @@ -176,10 +166,10 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: 4 / 5 - + >>> # Use binarytree.Node.pprint instead of print to display indexes >>> root.pprint(index=True) - + _________0-1_ / \ 1-2_____ 2-3 @@ -187,7 +177,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: _4-4 / 9-5 - + >>> # Return the node/subtree at index 9 >>> root[9] Node(5) @@ -195,7 +185,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: >>> # Replace the node/subtree at index 4 >>> root[4] = Node(6, left=Node(7), right=Node(8)) >>> root.pprint(index=True) - + ______________0-1_ / \ 1-2_____ 2-3 @@ -203,19 +193,19 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: _4-6_ / \ 9-7 10-8 - + >>> # Delete the node/subtree at index 1 >>> del root[1] >>> root.pprint(index=True) - + 0-1_ \ 2-3 - + Traverse the trees using different algorithms: -.. code-block:: python +.. doctest:: >>> from binarytree import Node >>> @@ -226,13 +216,13 @@ Traverse the trees using different algorithms: >>> root.left.right = Node(5) >>> >>> print(root) - + __1 / \ 2 3 / \ 4 5 - + >>> root.inorder [Node(4), Node(2), Node(5), Node(1), Node(3)] @@ -245,21 +235,23 @@ Traverse the trees using different algorithms: >>> root.levelorder [Node(1), Node(2), Node(3), Node(4), Node(5)] + >>> list(root) # Equivalent to root.levelorder + [Node(1), Node(2), Node(3), Node(4), Node(5)] `List representations`_ are also supported: .. _List representations: https://en.wikipedia.org/wiki/Binary_tree#Arrays - -.. code-block:: python +.. doctest:: >>> from binarytree import build >>> >>> # Build a tree from list representation - >>> root = build([7, 3, 2, 6, 9, None, 1, 5, 8]) + >>> values = [7, 3, 2, 6, 9, None, 1, 5, 8] + >>> root = build(values) >>> print(root) - + __7 / \ __3 2 @@ -267,10 +259,9 @@ Traverse the trees using different algorithms: 6 9 1 / \ 5 8 - + >>> # Convert the tree back to list representation - >>> list(root) + >>> root.values [7, 3, 2, 6, 9, None, 1, 5, 8] - -See :ref:`API Specification` for more details. \ No newline at end of file +See :doc:`specs` for more details. diff --git a/docs/api.rst b/docs/specs.rst similarity index 56% rename from docs/api.rst rename to docs/specs.rst index f95525e..c697c04 100644 --- a/docs/api.rst +++ b/docs/specs.rst @@ -1,13 +1,14 @@ API Specification ----------------- -This page contains the API specification for the -:ref:`binarytree.Node ` class, and utility functions -:ref:`binarytree.build `, -:ref:`binarytree.tree `, -:ref:`binarytree.bst ` and -:ref:`binarytree.heap `. +This page covers the API specification for the following classes and utility +functions: +* :class:`binarytree.Node` +* :func:`binarytree.build` +* :func:`binarytree.tree` +* :func:`binarytree.bst` +* :func:`binarytree.heap` Class: binarytree.Node ====================== @@ -16,27 +17,23 @@ Class: binarytree.Node :members: :special-members: :private-members: - :exclude-members: __weakref__, __repr__, __setattr__ - + :exclude-members: __weakref__, __repr__, __setattr__, __init__ Function: binarytree.build ========================== .. autofunction:: binarytree.build - Function: binarytree.tree ========================= .. autofunction:: binarytree.tree - Function: binarytree.bst ======================== .. autofunction:: binarytree.bst - Function: binarytree.heap ========================= diff --git a/setup.py b/setup.py index ed903da..7d6c79c 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,21 @@ from setuptools import setup, find_packages -from binarytree.version import __version__ +version = {} +with open("./binarytree/version.py") as fp: + exec(fp.read(), version) + +with open('./README.rst') as fp: + description = fp.read() setup( name='binarytree', description='Python Library for Learning Binary Trees', - version=__version__, + long_description=description, + version=version['__version__'], author='Joohwan Oh', author_email='joohwan.oh@outlook.com', url='https://github.com/joowani/binarytree', - packages=find_packages(), + packages=find_packages(exclude=['tests']), include_package_data=True, tests_require=['pytest', 'flake8'], license='MIT', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests.py b/tests/test_tree.py similarity index 76% rename from tests.py rename to tests/test_tree.py index 754261a..734049a 100644 --- a/tests.py +++ b/tests/test_tree.py @@ -1,43 +1,29 @@ -import sys +from __future__ import absolute_import, unicode_literals +import copy import random -try: - # noinspection PyCompatibility - from StringIO import StringIO -except ImportError: - from io import StringIO import pytest from binarytree import Node, build, tree, bst, heap from binarytree.exceptions import ( - InvalidNodeValueError, - InvalidNodeIndexError, - InvalidNodeTypeError, - OperationForbiddenError, + NodeValueError, + NodeIndexError, + NodeTypeError, + NodeModifyError, NodeNotFoundError, - InvalidTreeHeightError, - CyclicNodeReferenceError, + TreeHeightError, + NodeReferenceError, +) +from tests.utils import ( + builtin_print, + pprint_default, + pprint_with_index ) REPETITIONS = 20 -class CaptureOutput(list): - """Context manager to catch stdout.""" - - def __enter__(self): - self._original_stdout = sys.stdout - self._temp_stdout = StringIO() - sys.stdout = self._temp_stdout - return self - - def __exit__(self, *args): - lines = self._temp_stdout.getvalue().splitlines() - self.extend(line.rstrip() for line in lines) - sys.stdout = self._original_stdout - - @pytest.mark.order1 def test_node_set_attributes(): root = Node(1) @@ -72,29 +58,35 @@ def test_node_set_attributes(): assert root.left.right.value == 4 assert repr(last_node) == 'Node(4)' - with pytest.raises(InvalidNodeValueError): + with pytest.raises(NodeValueError) as err: # noinspection PyTypeChecker Node('this_is_not_an_integer') + assert str(err.value) == 'node value must be a number' - with pytest.raises(InvalidNodeTypeError): + 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' - with pytest.raises(InvalidNodeTypeError): + 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' - with pytest.raises(InvalidNodeValueError): + 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 number' - with pytest.raises(InvalidNodeTypeError): + with pytest.raises(NodeTypeError) as err: root.left = 'this_is_not_a_node' assert root.left is left_child + assert str(err.value) == 'left child must be a Node instance' - with pytest.raises(InvalidNodeTypeError): + with pytest.raises(NodeTypeError) as err: root.right = 'this_is_not_a_node' assert root.right is right_child + assert str(err.value) == 'right child must be a Node instance' @pytest.mark.order2 @@ -134,11 +126,11 @@ def test_tree_build(): with pytest.raises(NodeNotFoundError) as err: build([None, 1, 2]) - assert str(err.value) == 'Parent node missing at index 0' + assert str(err.value) == 'parent node missing at index 0' with pytest.raises(NodeNotFoundError) as err: build([1, None, 2, 3, 4]) - assert str(err.value) == 'Parent node missing at index 1' + assert str(err.value) == 'parent node missing at index 1' @pytest.mark.order3 @@ -159,12 +151,12 @@ def test_tree_get_node(): for index in [5, 6, 7, 8, 10]: with pytest.raises(NodeNotFoundError) as err: - _ = root[index] - assert str(err.value) == 'Node missing at index {}'.format(index) + assert root[index] + assert str(err.value) == 'node missing at index {}'.format(index) - with pytest.raises(InvalidNodeIndexError) as err: - _ = root[-1] - assert str(err.value) == 'The node index must be a non-negative integer' + with pytest.raises(NodeIndexError) as err: + assert root[-1] + assert str(err.value) == 'node index must be a non-negative int' @pytest.mark.order4 @@ -178,18 +170,19 @@ def test_tree_set_node(): new_node_1 = Node(7) new_node_2 = Node(8) + new_node_3 = Node(9) - with pytest.raises(OperationForbiddenError) as err: + with pytest.raises(NodeModifyError) as err: root[0] = new_node_1 - assert str(err.value) == 'Cannot modify the root node' + assert str(err.value) == 'cannot modify the root node' - with pytest.raises(InvalidNodeIndexError) as err: + with pytest.raises(NodeIndexError) as err: root[-1] = new_node_1 - assert str(err.value) == 'The node index must be a non-negative integer' + assert str(err.value) == 'node index must be a non-negative int' with pytest.raises(NodeNotFoundError) as err: root[100] = new_node_1 - assert str(err.value) == 'Parent node missing at index 49' + assert str(err.value) == 'parent node missing at index 49' root[10] = new_node_1 assert root.value == 1 @@ -209,9 +202,9 @@ def test_tree_set_node(): assert root.left.right.left is None assert root.left.right.right is None - root[1] = new_node_1 + root[1] = new_node_3 root[2] = new_node_2 - assert root.left is new_node_1 + assert root.left is new_node_3 assert root.right is new_node_2 @@ -224,21 +217,21 @@ def test_tree_del_node(): root.left.right = Node(5) root.left.right.left = Node(6) - with pytest.raises(OperationForbiddenError) as err: + with pytest.raises(NodeModifyError) as err: del root[0] - assert str(err.value) == 'Cannot delete the root node' + assert str(err.value) == 'cannot delete the root node' - with pytest.raises(InvalidNodeIndexError) as err: + with pytest.raises(NodeIndexError) as err: del root[-1] - assert str(err.value) == 'The node index must be a non-negative integer' + assert str(err.value) == 'node index must be a non-negative int' with pytest.raises(NodeNotFoundError) as err: del root[10] - assert str(err.value) == 'No node to delete at index 10' + assert str(err.value) == 'no node to delete at index 10' with pytest.raises(NodeNotFoundError) as err: del root[100] - assert str(err.value) == 'No node to delete at index 100' + assert str(err.value) == 'no node to delete at index 100' del root[3] assert root.left.left is None @@ -278,55 +271,40 @@ def test_tree_del_node(): @pytest.mark.order6 def test_tree_print_no_index(): - - def class_pprint_method(values): - root = build(values) - with CaptureOutput() as output: - root.pprint(index=False, delimiter='-') - assert output[0] == '' and output[-1] == '' - return [line for line in output if line != ''] - - def builtin_print_function(values): - root = build(values) - with CaptureOutput() as output: - print(root) - assert output[0] == '' and output[-1] == '' - return [line for line in output if line != ''] - - for print_without_index in [builtin_print_function, class_pprint_method]: - lines = print_without_index([1]) + for printer in [builtin_print, pprint_default]: + lines = printer([1]) assert lines == ['1'] - lines = print_without_index([1, 2]) + lines = printer([1, 2]) assert lines == [' 1', ' /', '2'] - lines = print_without_index([1, None, 3]) + lines = printer([1, None, 3]) assert lines == ['1', ' \\', ' 3'] - lines = print_without_index([1, 2, 3]) + lines = printer([1, 2, 3]) assert lines == [' 1', ' / \\', '2 3'] - lines = print_without_index([1, 2, 3, None, 5]) + lines = printer([1, 2, 3, None, 5]) assert lines == [' __1', ' / \\', '2 3', ' \\', ' 5'] - lines = print_without_index([1, 2, 3, None, 5, 6]) + lines = printer([1, 2, 3, None, 5, 6]) assert lines == [' __1__', ' / \\', '2 3', ' \\ /', ' 5 6'] - lines = print_without_index([1, 2, 3, None, 5, 6, 7]) + lines = printer([1, 2, 3, None, 5, 6, 7]) assert lines == [' __1__', ' / \\', '2 3', ' \\ / \\', ' 5 6 7'] - lines = print_without_index([1, 2, 3, 8, 5, 6, 7]) + lines = printer([1, 2, 3, 8, 5, 6, 7]) assert lines == [' __1__', ' / \\', ' 2 3', @@ -336,47 +314,39 @@ def builtin_print_function(values): @pytest.mark.order7 def test_tree_print_with_index(): - - def print_with_index(values): - root = build(values) - with CaptureOutput() as output: - root.pprint(index=True, delimiter=':') - assert output[0] == '' and output[-1] == '' - return [line for line in output if line != ''] - - lines = print_with_index([1]) + lines = pprint_with_index([1]) assert lines == ['0:1'] - lines = print_with_index([1, 2]) + lines = pprint_with_index([1, 2]) assert lines == [' _0:1', ' /', '1:2'] - lines = print_with_index([1, None, 3]) + lines = pprint_with_index([1, None, 3]) assert lines == ['0:1_', ' \\', ' 2:3'] - lines = print_with_index([1, 2, 3]) + lines = pprint_with_index([1, 2, 3]) assert lines == [' _0:1_', ' / \\', '1:2 2:3'] - lines = print_with_index([1, 2, 3, None, 5]) + lines = pprint_with_index([1, 2, 3, None, 5]) assert lines == [' _____0:1_', ' / \\', '1:2_ 2:3', ' \\', ' 4:5'] - lines = print_with_index([1, 2, 3, None, 5, 6]) + lines = pprint_with_index([1, 2, 3, None, 5, 6]) assert lines == [' _____0:1_____', ' / \\', '1:2_ _2:3', ' \\ /', ' 4:5 5:6'] - lines = print_with_index([1, 2, 3, None, 5, 6, 7]) + lines = pprint_with_index([1, 2, 3, None, 5, 6, 7]) assert lines == [' _____0:1_____', ' / \\', '1:2_ _2:3_', ' \\ / \\', ' 4:5 5:6 6:7'] - lines = print_with_index([1, 2, 3, 8, 5, 6, 7]) + lines = pprint_with_index([1, 2, 3, 8, 5, 6, 7]) assert lines == [' _____0:1_____', ' / \\', ' _1:2_ _2:3_', @@ -413,23 +383,23 @@ def __setattr__(self, attr, value): root = TestNode(1) root.left = 'not_a_node' - with pytest.raises(InvalidNodeTypeError) as err: + with pytest.raises(NodeTypeError) as err: root.validate() - assert str(err.value) == 'Invalid node instance at index 1' + assert str(err.value) == 'invalid node instance at index 1' root = TestNode(1) root.right = TestNode(2) root.right.value = 'not_an_integer' - with pytest.raises(InvalidNodeValueError) as err: + with pytest.raises(NodeValueError) as err: root.validate() - assert str(err.value) == 'Invalid node value at index 2' + assert str(err.value) == 'invalid node value at index 2' root = TestNode(1) root.left = TestNode(2) root.left.right = root - with pytest.raises(CyclicNodeReferenceError) as err: + with pytest.raises(NodeReferenceError) as err: root.validate() - assert str(err.value) == 'Cyclic node reference at index 4' + assert str(err.value) == 'cyclic node reference at index 4' @pytest.mark.order9 @@ -702,33 +672,33 @@ def test_tree_traversal(): def test_tree_list_representation(): root = Node(1) - assert list(root) == [1] + assert root.values == [1] root.right = Node(3) - assert list(root) == [1, None, 3] + assert root.values == [1, None, 3] root.left = Node(2) - assert list(root) == [1, 2, 3] + assert root.values == [1, 2, 3] root.right.left = Node(4) - assert list(root) == [1, 2, 3, None, None, 4] + assert root.values == [1, 2, 3, None, None, 4] root.right.right = Node(5) - assert list(root) == [1, 2, 3, None, None, 4, 5] + assert root.values == [1, 2, 3, None, None, 4, 5] root.left.left = Node(6) - assert list(root) == [1, 2, 3, 6, None, 4, 5] + assert root.values == [1, 2, 3, 6, None, 4, 5] root.left.right = Node(7) - assert list(root) == [1, 2, 3, 6, 7, 4, 5] + assert root.values == [1, 2, 3, 6, 7, 4, 5] @pytest.mark.order11 def test_tree_generation(): for invalid_height in ['foo', -1, None]: - with pytest.raises(InvalidTreeHeightError) as err: + with pytest.raises(TreeHeightError) as err: tree(height=invalid_height) - assert str(err.value) == 'The height must be an integer between 0 - 9' + assert str(err.value) == 'height must be an int between 0 - 9' root = tree(height=0) root.validate() @@ -756,9 +726,9 @@ def test_tree_generation(): @pytest.mark.order12 def test_bst_generation(): for invalid_height in ['foo', -1, None]: - with pytest.raises(InvalidTreeHeightError) as err: + with pytest.raises(TreeHeightError) as err: bst(height=invalid_height) - assert str(err.value) == 'The height must be an integer between 0 - 9' + assert str(err.value) == 'height must be an int between 0 - 9' root = bst(height=0) root.validate() @@ -793,9 +763,9 @@ def test_bst_generation(): @pytest.mark.order13 def test_heap_generation(): for invalid_height in ['foo', -1, None]: - with pytest.raises(InvalidTreeHeightError) as err: + with pytest.raises(TreeHeightError) as err: heap(height=invalid_height) - assert str(err.value) == 'The height must be an integer between 0 - 9' + assert str(err.value) == 'height must be an int between 0 - 9' root = heap(height=0) root.validate() @@ -830,3 +800,70 @@ def test_heap_generation(): assert root.is_balanced is True assert root.is_strict is True assert root.height == random_height + + +@pytest.mark.order13 +def test_heap_float_values(): + root = Node(1.0) + root.left = Node(0.5) + root.right = Node(1.5) + + assert root.height == 1 + assert root.is_balanced is True + assert root.is_bst is True + assert root.is_complete is True + assert root.is_max_heap is False + assert root.is_min_heap is False + assert root.is_perfect is True + assert root.is_strict is True + assert root.leaf_count == 2 + assert root.max_leaf_depth == 1 + assert root.max_node_value == 1.5 + assert root.min_leaf_depth == 1 + assert root.min_node_value == 0.5 + assert root.size == 3 + + for printer in [builtin_print, pprint_default]: + lines = printer([1.0]) + assert lines == ['1.0'] + lines = printer([1.0, 2.0]) + assert lines == [' _1.0', + ' /', + '2.0'] + lines = printer([1.0, None, 3.0]) + assert lines == ['1.0_', + ' \\', + ' 3.0'] + lines = printer([1.0, 2.0, 3.0]) + assert lines == [' _1.0_', + ' / \\', + '2.0 3.0'] + lines = printer([1.0, 2.0, 3.0, None, 5.0]) + assert lines == [' _____1.0_', + ' / \\', + '2.0_ 3.0', + ' \\', + ' 5.0'] + + for builder in [tree, bst, heap]: + for _ in range(REPETITIONS): + root = builder() + root_copy = copy.deepcopy(root) + + for node in root: + node.value += 0.1 + + assert root.height == root_copy.height + assert root.is_balanced == root_copy.is_balanced + assert root.is_bst == root_copy.is_bst + assert root.is_complete == root_copy.is_complete + assert root.is_max_heap == root_copy.is_max_heap + assert root.is_min_heap == root_copy.is_min_heap + assert root.is_perfect == root_copy.is_perfect + assert root.is_strict == root_copy.is_strict + assert root.leaf_count == root_copy.leaf_count + assert root.max_leaf_depth == root_copy.max_leaf_depth + assert root.max_node_value == root_copy.max_node_value + 0.1 + assert root.min_leaf_depth == root_copy.min_leaf_depth + assert root.min_node_value == root_copy.min_node_value + 0.1 + assert root.size == root_copy.size diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..9911bd2 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import, unicode_literals + +import sys +try: + # noinspection PyCompatibility + from StringIO import StringIO +except ImportError: + from io import StringIO + +from binarytree import build + + +class CaptureOutput(list): + """Context manager to catch stdout.""" + + def __enter__(self): + self._original_stdout = sys.stdout + self._temp_stdout = StringIO() + sys.stdout = self._temp_stdout + return self + + def __exit__(self, *args): + lines = self._temp_stdout.getvalue().splitlines() + self.extend(line.rstrip() for line in lines) + sys.stdout = self._original_stdout + + +def pprint_default(values): + """Helper function for testing Node.pprint with default arguments.""" + root = build(values) + 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): + """Helper function for testing Node.pprint with indexes.""" + root = build(values) + 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): + """Helper function for testing builtin print on Node.""" + root = build(values) + with CaptureOutput() as output: + print(root) + assert output[0] == '' and output[-1] == '' + return [line for line in output if line != '']