diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..b4a7986 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,47 @@ +name: Build +on: + workflow_dispatch: + inputs: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] +jobs: + build: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + python-version: [ 3.6, 3.7, 3.8, 3.9 ] + steps: + - uses: actions/checkout@v2 + - name: Fetch complete history for all tags and branches + run: git fetch --prune --unshallow + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Setup pip + run: python -m pip install --upgrade pip setuptools wheel + - name: Install package + run: pip install .[dev] + - name: Run black + run: black --check . + - name: Run flake8 + run: flake8 . + - name: Run isort + run: isort --check --profile=black . + - name: Run mypy + run: mypy . + - name: Run pytest + run: py.test --cov=./ --cov-report=xml + - name: Run Sphinx doctest + run: python -m sphinx -b doctest docs docs/_build + - name: Run Sphinx HTML + run: python -m sphinx -b html -W docs docs/_build + - name: Upload coverge to Codecov + uses: codecov/codecov-action@v1 + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000..bafb67f --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,22 @@ +name: CodeQL +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + schedule: + - cron: '21 2 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: 'python' + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml new file mode 100644 index 0000000..5adc754 --- /dev/null +++ b/.github/workflows/pypi.yaml @@ -0,0 +1,31 @@ +name: Upload to PyPI +on: + release: + types: [created] +jobs: + upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Fetch complete history for all tags and branches + run: git fetch --prune --unshallow + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine setuptools_scm[toml] + - name: Build distribution + run: python setup.py sdist bdist_wheel + - name: Publish to PyPI Test + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }} + run: twine upload --repository testpypi dist/* + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload --repository pypi dist/* diff --git a/.gitignore b/.gitignore index 6f5f4b8..f957230 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,9 @@ ENV/ # Rope project settings .ropeproject + +# PyCharm .idea/ + +# setuptools-scm +binarytree/version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fc7f652 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: mixed-line-ending + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + - repo: https://github.com/timothycrosley/isort + rev: 5.7.0 + hooks: + - id: isort + args: [ --profile, black ] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.790 + hooks: + - id: mypy + args: [ --ignore-missing-imports ] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1c8436e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -sudo: false -language: python -python: - - "2.7" - - "3.5" - - "3.6" - - "3.7" - - "3.8" -install: - - pip install flake8 mock pytest pytest-cov coveralls - - pip install sphinx sphinx_rtd_theme - - pip install . -script: - - 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/LICENSE b/LICENSE index 6ac37cb..65368b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Joohwan Oh +Copyright (c) 2016,2017,2018,2019,2020,2021 Joohwan Oh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in index 95dea49..c2e5560 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ -include README.rst LICENSE +include README.md LICENSE prune tests +recursive-include gifs diff --git a/README.md b/README.md new file mode 100644 index 0000000..97ccf4e --- /dev/null +++ b/README.md @@ -0,0 +1,315 @@ +# Binarytree: Python Library for Studying Binary Trees + +![Build](https://github.com/joowani/binarytree/workflows/Build/badge.svg) +![CodeQL](https://github.com/joowani/binarytree/workflows/CodeQL/badge.svg) +[![codecov](https://codecov.io/gh/joowani/binarytree/branch/master/graph/badge.svg?token=B6sFAVxxsk)](https://codecov.io/gh/joowani/binarytree) +[![PyPI version](https://badge.fury.io/py/binarytree.svg)](https://badge.fury.io/py/binarytree) +[![GitHub license](https://img.shields.io/github/license/joowani/binarytree?color=brightgreen)](https://github.com/joowani/binarytree/blob/main/LICENSE) +![Python version](https://img.shields.io/badge/python-3.6%2B-blue) + +Are you studying binary trees for your next exam, assignment or technical interview? + +**Binarytree** is Python library which lets you generate, visualize, inspect and +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. + +![](gifs/demo.gif) + +**New in version 6.0.0**: You can now use binarytree with +[Graphviz](https://graphviz.org) and [Jupyter Notebooks](https://jupyter.org) +([documentation](https://binarytree.readthedocs.io/en/main/graphviz.html)): + +![](gifs/jupyter.gif) + +## Requirements + +Python 3.6+ + +## Installation + +Install via [pip](https://pip.pypa.io): + +```shell +pip install binarytree +``` + +For [conda](https://docs.conda.io) users: + +```shell +conda install binarytree -c conda-forge +``` + +## Getting Started + +Binarytree uses the following class to represent a node: + +```python +class Node: + + def __init__(self, value, left=None, right=None): + self.value = value # The node value (integer) + self.left = left # Left child + self.right = right # Right child +``` + +Generate and pretty-print various types of binary trees: + +```python +from binarytree import tree, bst, heap + +# Generate a random binary tree and return its root node +my_tree = tree(height=3, is_perfect=False) + +# Generate a random BST and return its root node +my_bst = bst(height=3, is_perfect=True) + +# Generate a random max heap and return its root node +my_heap = heap(height=3, is_max=True, is_perfect=False) + +# Pretty-print the trees in stdout +print(my_tree) +# +# _______1_____ +# / \ +# 4__ ___3 +# / \ / \ +# 0 9 13 14 +# / \ \ +# 7 10 2 +# +print(my_bst) +# +# ______7_______ +# / \ +# __3__ ___11___ +# / \ / \ +# 1 5 9 _13 +# / \ / \ / \ / \ +# 0 2 4 6 8 10 12 14 +# +print(my_heap) +# +# _____14__ +# / \ +# ____13__ 9 +# / \ / \ +# 12 7 3 8 +# / \ / +# 0 10 6 +# +``` + +Build your own trees: + +```python +from binarytree import Node + +root = Node(1) +root.left = Node(2) +root.right = Node(3) +root.left.right = Node(4) + +print(root) +# +# __1 +# / \ +# 2 3 +# \ +# 4 +# +``` + +Inspect tree properties: + +```python +from binarytree import Node + +root = Node(1) +root.left = Node(2) +root.right = Node(3) +root.left.left = Node(4) +root.left.right = Node(5) + +print(root) +# +# __1 +# / \ +# 2 3 +# / \ +# 4 5 +# +assert root.height == 2 +assert root.is_balanced is True +assert root.is_bst is False +assert root.is_complete is True +assert root.is_max_heap is False +assert root.is_min_heap is True +assert root.is_perfect is False +assert root.is_strict is True +assert root.leaf_count == 3 +assert root.max_leaf_depth == 2 +assert root.max_node_value == 5 +assert root.min_leaf_depth == 1 +assert root.min_node_value == 1 +assert root.size == 5 + +# See all properties at once: +assert 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 +} + +print(root.leaves) +# [Node(3), Node(4), Node(5)] + +print(root.levels) +# [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] +``` +Use [level-order (breadth-first)](https://en.wikipedia.org/wiki/Tree_traversal#Breadth-first_search) +indexes to manipulate nodes: + +```python +from binarytree import Node + +root = Node(1) # index: 0, value: 1 +root.left = Node(2) # index: 1, value: 2 +root.right = Node(3) # index: 2, value: 3 +root.left.right = Node(4) # index: 4, value: 4 +root.left.right.left = Node(5) # index: 9, value: 5 + +print(root) +# +# ____1 +# / \ +# 2__ 3 +# \ +# 4 +# / +# 5 +# +root.pprint(index=True) +# +# _________0-1_ +# / \ +# 1-2_____ 2-3 +# \ +# _4-4 +# / +# 9-5 +# +print(root[9]) +# Node(5) + +# 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 +# \ +# _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 trees using different algorithms: + +```python +from binarytree import Node + +root = Node(1) +root.left = Node(2) +root.right = Node(3) +root.left.left = Node(4) +root.left.right = Node(5) + +print(root) +# +# __1 +# / \ +# 2 3 +# / \ +# 4 5 +# +print(root.inorder) +# [Node(4), Node(2), Node(5), Node(1), Node(3)] + +print(root.preorder) +# [Node(1), Node(2), Node(4), Node(5), Node(3)] + +print(root.postorder) +# [Node(4), Node(5), Node(2), Node(3), Node(1)] + +print(root.levelorder) +# [Node(1), Node(2), Node(3), Node(4), Node(5)] + +print(list(root)) # Equivalent to root.levelorder +# [Node(1), Node(2), Node(3), Node(4), Node(5)] +``` + +[List representations](https://en.wikipedia.org/wiki/Binary_tree#Arrays) are also +supported: + +```python +from binarytree import build + +# Build a tree from list representation +values = [7, 3, 2, 6, 9, None, 1, 5, 8] +root = build(values) +print(root) +# +# __7 +# / \ +# __3 2 +# / \ \ +# 6 9 1 +# / \ +# 5 8 +# + +# Go back to list representation +print(root.values) +# [7, 3, 2, 6, 9, None, 1, 5, 8] +``` + +Check out the [documentation](http://binarytree.readthedocs.io) for more details. + +Contributing +============ + +Set up dev environment: + +```shell +cd ~/your/binarytree/clone # Activate venv if you have one (recommended) +pip install -e .[dev] # Install dev dependencies (black, mypy, pre-commit etc.) +pre-commit install # Install git pre-commit hooks +``` + +Run unit tests with coverage: + +```shell +py.test --cov=binarytree --cov-report=html +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index c1a1b50..0000000 --- a/README.rst +++ /dev/null @@ -1,352 +0,0 @@ -Binarytree: Python Library for Studying Binary Trees ----------------------------------------------------- - -.. image:: https://travis-ci.org/joowani/binarytree.svg?branch=master - :target: https://travis-ci.org/joowani/binarytree - :alt: Build Status - -.. image:: https://badge.fury.io/py/binarytree.svg - :target: https://badge.fury.io/py/binarytree - :alt: Package Version - -.. image:: https://img.shields.io/badge/python-2.7%2C%203.5%2C%203.6%2C%203.7%2C%203.8-blue.svg - :target: https://github.com/joowani/binarytree - :alt: Python Versions - -.. image:: https://coveralls.io/repos/github/joowani/binarytree/badge.svg?branch=master - :target: https://coveralls.io/github/joowani/binarytree?branch=master - :alt: Test Coverage - -.. image:: https://img.shields.io/github/issues/joowani/binarytree.svg - :target: https://github.com/joowani/binarytree/issues - :alt: Issues Open - -.. image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://raw.githubusercontent.com/joowani/binarytree/master/LICENSE - :alt: MIT License - -| - -.. image:: https://user-images.githubusercontent.com/2701938/34109703-4a8810aa-e3b9-11e7-8138-68eec47cfddb.gif - :alt: Demo GIF - -Introduction -============ - -Are you studying binary trees for your next exam, assignment or technical interview? - -**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. - -Requirements -============ - -- Python 2.7+ or 3.4+ - -Installation -============ - -To install a stable version from PyPi_: - -.. code-block:: bash - - ~$ 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. - -.. _PyPi: https://pypi.python.org/pypi/binarytree -.. _GitHub: https://github.com/joowani/binarytree - -Getting Started -=============== - -By default, **binarytree** uses the following class to represent a node: - -.. code-block:: python - - class Node(object): - - def __init__(self, value, left=None, right=None): - self.val = value # The node value - self.left = left # Left child - self.right = right # Right child - -Generate and pretty-print various types of binary trees: - -.. code-block:: python - - >>> from binarytree import tree, bst, heap - >>> - >>> # Generate a random binary tree and return its root node - >>> my_tree = tree(height=3, is_perfect=False) - >>> - >>> # Generate a random BST and return its root node - >>> my_bst = bst(height=3, is_perfect=True) - >>> - >>> # Generate a random max heap and return its root node - >>> my_heap = heap(height=3, is_max=True, is_perfect=False) - >>> - >>> # Pretty-print the trees in stdout - >>> print(my_tree) - # - # _______1_____ - # / \ - # 4__ ___3 - # / \ / \ - # 0 9 13 14 - # / \ \ - # 7 10 2 - # - >>> print(my_bst) - # - # ______7_______ - # / \ - # __3__ ___11___ - # / \ / \ - # 1 5 9 _13 - # / \ / \ / \ / \ - # 0 2 4 6 8 10 12 14 - # - >>> print(my_heap) - # - # _____14__ - # / \ - # ____13__ 9 - # / \ / \ - # 12 7 3 8 - # / \ / - # 0 10 6 - # - -Use the `binarytree.Node`_ class to build your own trees: - -.. _binarytree.Node: - http://binarytree.readthedocs.io/en/latest/specs.html#class-binarytree-node - -.. code-block:: python - - >>> from binarytree import Node - >>> - >>> root = Node(1) - >>> root.left = Node(2) - >>> root.right = Node(3) - >>> root.left.right = Node(4) - >>> - >>> print(root) - # - # __1 - # / \ - # 2 3 - # \ - # 4 - # - -Inspect tree properties: - -.. code-block:: python - - >>> from binarytree import Node - >>> - >>> root = Node(1) - >>> root.left = Node(2) - >>> root.right = Node(3) - >>> root.left.left = Node(4) - >>> root.left.right = Node(5) - >>> - >>> print(root) - # - # __1 - # / \ - # 2 3 - # / \ - # 4 5 - # - >>> root.height - 2 - >>> root.is_balanced - True - >>> root.is_bst - False - >>> root.is_complete - True - >>> root.is_max_heap - False - >>> root.is_min_heap - True - >>> root.is_perfect - False - >>> root.is_strict - True - >>> root.leaf_count - 3 - >>> root.max_leaf_depth - 2 - >>> root.max_node_value - 5 - >>> root.min_leaf_depth - 1 - >>> root.min_node_value - 1 - >>> root.size - 5 - - >>> root.properties # To see all at once: - {'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} - - >>> root.leaves - [Node(3), Node(4), Node(5)] - - >>> root.levels - [[Node(1)], [Node(2), Node(3)], [Node(4), Node(5)]] - -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 - - >>> from binarytree import Node - >>> - >>> root = Node(1) # index: 0, value: 1 - >>> root.left = Node(2) # index: 1, value: 2 - >>> root.right = Node(3) # index: 2, value: 3 - >>> root.left.right = Node(4) # index: 4, value: 4 - >>> root.left.right.left = Node(5) # index: 9, value: 5 - >>> - >>> print(root) - # - # ____1 - # / \ - # 2__ 3 - # \ - # 4 - # / - # 5 - # - >>> # Use binarytree.Node.pprint instead of print to display indexes - >>> root.pprint(index=True) - # - # _________0-1_ - # / \ - # 1-2_____ 2-3 - # \ - # _4-4 - # / - # 9-5 - # - >>> # Return the node/subtree at index 9 - >>> root[9] - Node(5) - - >>> # 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 - # \ - # _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 - - >>> from binarytree import Node - >>> - >>> root = Node(1) - >>> root.left = Node(2) - >>> root.right = Node(3) - >>> root.left.left = Node(4) - >>> root.left.right = Node(5) - >>> - >>> print(root) - # - # __1 - # / \ - # 2 3 - # / \ - # 4 5 - # - >>> root.inorder - [Node(4), Node(2), Node(5), Node(1), Node(3)] - - >>> root.preorder - [Node(1), Node(2), Node(4), Node(5), Node(3)] - - >>> root.postorder - [Node(4), Node(5), Node(2), Node(3), Node(1)] - - >>> 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 - - >>> from binarytree import build - >>> - >>> # Build a tree from list representation - >>> values = [7, 3, 2, 6, 9, None, 1, 5, 8] - >>> root = build(values) - >>> print(root) - # - # __7 - # / \ - # __3 2 - # / \ \ - # 6 9 1 - # / \ - # 5 8 - # - >>> # Convert the tree back to list representation - >>> 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 diff --git a/binarytree/__init__.py b/binarytree/__init__.py index 2681f80..43981c3 100644 --- a/binarytree/__init__.py +++ b/binarytree/__init__.py @@ -1,324 +1,28 @@ -from __future__ import absolute_import, unicode_literals, division - -__all__ = ['Node', 'tree', 'bst', 'heap', 'build', 'get_parent'] +__all__ = ["Node", "tree", "bst", "heap", "build", "get_parent", "__version__"] import heapq import random -import numbers +from typing import List, Optional, Union + +from graphviz import Digraph, nohtml +from pkg_resources import get_distribution from binarytree.exceptions import ( - TreeHeightError, - NodeValueError, NodeIndexError, - NodeTypeError, NodeModifyError, NodeNotFoundError, NodeReferenceError, + NodeTypeError, + NodeValueError, + TreeHeightError, ) -LEFT = 'left' -RIGHT = 'right' -VAL = 'val' -VALUE = 'value' - - -def _is_balanced(root): - """Return the tree height + 1 if balanced, -1 otherwise. - - :param root: Root node of the binary tree. - :type root: binarytree.Node - :return: Height if the binary tree is balanced, -1 otherwise. - :rtype: int - """ - if root is None: - return 0 - left = _is_balanced(root.left) - if left < 0: - return -1 - right = _is_balanced(root.right) - if right < 0: - return -1 - return -1 if abs(left - right) > 1 else max(left, right) + 1 - - -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: Root node of the binary tree. - :type root: binarytree.Node - :param min_value: Minimum node value seen. - :type min_value: int | float - :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 - """ - if root is None: - return True - return ( - min_value < root.val < max_value and - _is_bst(root.left, min_value, root.val) and - _is_bst(root.right, root.val, max_value) - ) - - -def _is_symmetric(root): - """Check if the binary tree is symmetric. - - :param root: Root node of the binary tree. - :type root: binarytree.Node - :return: True if the binary tree is symmetric, False otherwise. - :rtype: bool - """ - - def symmetric_helper(left_subtree, right_subtree): - if left_subtree is None and right_subtree is None: - return True - if left_subtree is None or right_subtree is None: - return False - return ( - left_subtree.val == right_subtree.val and - symmetric_helper(left_subtree.left, right_subtree.right) and - symmetric_helper(left_subtree.right, right_subtree.left) - ) - - return symmetric_helper(root, root) - - -def _validate_tree_height(height): - """Check if the height of the binary tree is valid. - - :param height: Height of the binary tree (must be 0 - 9 inclusive). - :type height: int - :raise binarytree.exceptions.TreeHeightError: If height is invalid. - """ - if not (isinstance(height, int) and 0 <= height <= 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. - - :param height: Height of the BST. - :type height: int - :return: Root node of the BST. - :rtype: binarytree.Node - """ - max_node_count = 2 ** (height + 1) - 1 - node_values = list(range(max_node_count)) - return _build_bst_from_sorted_values(node_values) - - -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 | float] - :return: Root node of the BST. - :rtype: binarytree.Node - """ - if len(sorted_values) == 0: - return None - mid_index = len(sorted_values) // 2 - root = Node(sorted_values[mid_index]) - root.left = _build_bst_from_sorted_values(sorted_values[:mid_index]) - root.right = _build_bst_from_sorted_values(sorted_values[mid_index + 1:]) - return root - - -def _generate_random_leaf_count(height): - """Return a random leaf count for building binary trees. - - :param height: Height of the binary tree. - :type height: int - :return: Random leaf count. - :rtype: int - """ - max_leaf_count = 2 ** height - half_leaf_count = max_leaf_count // 2 - - # A very naive way of mimicking normal distribution - roll_1 = random.randint(0, half_leaf_count) - roll_2 = random.randint(0, max_leaf_count - half_leaf_count) - return roll_1 + roll_2 or half_leaf_count - - -def _generate_random_node_values(height): - """Return random node values for building binary trees. - - :param height: Height of the binary tree. - :type height: int - :return: Randomly generated node values. - :rtype: [int] - """ - max_node_count = 2 ** (height + 1) - 1 - node_values = list(range(max_node_count)) - random.shuffle(node_values) - return node_values - - -def _build_tree_string(root, curr_index, index=False, delimiter='-'): - """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 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: Root node of the binary tree. - :type root: binarytree.Node - :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). - :type index: bool - :param delimiter: Delimiter character between the node index and the node - value (default: '-'). - :type delimiter: - :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#Breadth-first_search - """ - if root is None: - return [], 0, 0, 0 - - line1 = [] - line2 = [] - if index: - node_repr = '{}{}{}'.format(curr_index, delimiter, root.val) - else: - node_repr = str(root.val) - - new_root_width = gap_size = len(node_repr) - - # Get the left and right sub-boxes, their widths, and root repr positions - l_box, l_box_width, l_root_start, l_root_end = \ - _build_tree_string(root.left, 2 * curr_index + 1, index, 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 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)) - line1.append('_' * (l_box_width - l_root)) - line2.append(' ' * l_root + '/') - line2.append(' ' * (l_box_width - l_root)) - new_root_start = l_box_width + 1 - gap_size += 1 - else: - new_root_start = 0 - - # Draw the representation of the current root node - line1.append(node_repr) - line2.append(' ' * new_root_width) - - # 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) - line1.append(' ' * (r_box_width - r_root + 1)) - line2.append(' ' * r_root + '\\') - line2.append(' ' * (r_box_width - r_root)) - gap_size += 1 - new_root_end = new_root_start + new_root_width - 1 - - # Combine the left and right sub-boxes with the branches drawn above - gap = ' ' * gap_size - new_box = [''.join(line1), ''.join(line2)] - for i in range(max(len(l_box), len(r_box))): - l_line = l_box[i] if i < len(l_box) else ' ' * l_box_width - r_line = r_box[i] if i < len(r_box) else ' ' * r_box_width - new_box.append(l_line + gap + r_line) +__version__ = get_distribution("binarytree").version - # 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: Root node of the binary tree. - :type root: binarytree.Node - :return: Binary tree properties. - :rtype: dict - """ - is_descending = True - is_ascending = True - min_node_value = root.val - max_node_value = root.val - size = 0 - leaf_count = 0 - min_leaf_depth = 0 - max_leaf_depth = -1 - is_strict = True - is_complete = True - current_level = [root] - non_full_node_seen = False - - while len(current_level) > 0: - max_leaf_depth += 1 - next_level = [] - - for node in current_level: - size += 1 - val = node.val - min_node_value = min(val, min_node_value) - max_node_value = max(val, max_node_value) - - # 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 - leaf_count += 1 - - if node.left is not None: - if node.left.val > val: - is_descending = False - elif node.left.val < val: - is_ascending = False - next_level.append(node.left) - is_complete = not non_full_node_seen - else: - non_full_node_seen = True - - if node.right is not None: - if node.right.val > val: - is_descending = False - elif node.right.val < val: - is_ascending = False - next_level.append(node.right) - is_complete = not non_full_node_seen - else: - non_full_node_seen = True - - # If we see a node with only one child, it is not strict - is_strict &= (node.left is None) == (node.right is None) - - current_level = next_level - - return { - 'height': max_leaf_depth, - 'size': size, - 'is_max_heap': is_complete and is_descending, - 'is_min_heap': is_complete and is_ascending, - 'is_perfect': leaf_count == 2 ** max_leaf_depth, - 'is_strict': is_strict, - 'is_complete': is_complete, - 'leaf_count': leaf_count, - 'min_node_value': min_node_value, - 'max_node_value': max_node_value, - 'min_leaf_depth': min_leaf_depth, - 'max_leaf_depth': max_leaf_depth, - } +LEFT = "left" +RIGHT = "right" +VAL = "val" +VALUE = "value" def get_parent(root, child): @@ -372,16 +76,16 @@ def get_parent(root, child): return None -class Node(object): +class Node: """Represents a binary tree node. This class provides methods and properties for managing the current node, - 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 as - well as all its descendants. + and the binary tree in which the node is the root. When a docstring in + 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 | numbers.Number + :type value: int | float :param left: Left child node (default: None). :type left: binarytree.Node :param right: Right child node (default: None). @@ -392,13 +96,21 @@ class Node(object): (e.g. int, float). """ - def __init__(self, value, left=None, right=None): - if not isinstance(value, numbers.Number): - raise NodeValueError('node value must be a number') + def __init__( + self, + value: Union[float, int], + left: Optional["Node"] = None, + right: Optional["Node"] = None, + ): + + if not isinstance(value, (float, int)): + raise NodeValueError("node value must be a float or int") + if left is not None and not isinstance(left, Node): - raise NodeTypeError('left child must be a Node instance') + raise NodeTypeError("left child must be a Node instance") + if right is not None and not isinstance(right, Node): - raise NodeTypeError('right child must be a Node instance') + raise NodeTypeError("right child must be a Node instance") self.value = self.val = value self.left = left @@ -419,7 +131,7 @@ def __repr__(self): >>> Node(1) Node(1) """ - return 'Node({})'.format(self.val) + return "Node({})".format(self.val) def __str__(self): """Return the pretty-print string for the binary tree. @@ -454,8 +166,8 @@ def __str__(self): .. _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)) + lines = _build_tree_string(self, 0, False, "-")[0] + return "\n" + "\n".join((line.rstrip() for line in lines)) def __setattr__(self, attr, obj): """Modified version of ``__setattr__`` with extra sanity checking. @@ -491,21 +203,21 @@ def __setattr__(self, attr, obj): >>> node.val = 'invalid' # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - NodeValueError: node value must be a number + NodeValueError: node value must be a float or int """ if attr == LEFT: if obj is not None and not isinstance(obj, Node): - raise NodeTypeError('left child must be a Node instance') + raise NodeTypeError("left child must be a Node instance") elif attr == RIGHT: if obj is not None and not isinstance(obj, Node): - raise NodeTypeError('right child must be a Node instance') + raise NodeTypeError("right child must be a Node instance") elif attr == VALUE: - if not isinstance(obj, numbers.Number): - raise NodeValueError('node value must be a number') + if not isinstance(obj, (float, int)): + raise NodeValueError("node value must be a float or int") object.__setattr__(self, VAL, obj) elif attr == VAL: - if not isinstance(obj, numbers.Number): - raise NodeValueError('node value must be a number') + if not isinstance(obj, (float, int)): + raise NodeValueError("node value must be a float or int") object.__setattr__(self, VALUE, obj) object.__setattr__(self, attr, obj) @@ -539,7 +251,7 @@ def __iter__(self): / \\ 4 5 - >>> [node for node in root] + >>> list(root) [Node(1), Node(2), Node(3), Node(4), Node(5)] """ current_level = [self] @@ -576,7 +288,7 @@ def __len__(self): .. note:: This method is equivalent to :attr:`binarytree.Node.size`. """ - return self.properties['size'] + return self.properties["size"] def __getitem__(self, index): """Return the node (or subtree) at the given level-order_ index. @@ -613,8 +325,7 @@ def __getitem__(self, index): NodeNotFoundError: node missing at index 3 """ if not isinstance(index, int) or index < 0: - raise NodeIndexError( - 'node index must be a non-negative int') + raise NodeIndexError("node index must be a non-negative int") current_level = [self] current_index = 0 @@ -633,15 +344,17 @@ def __getitem__(self, index): current_index += 1 if node is None: - next_level.extend((None, None)) + next_level.append(None) + next_level.append(None) continue - next_level.extend((node.left, node.right)) + next_level.append(node.left) + next_level.append(node.right) if node.left is not None or node.right is not None: has_more_nodes = True current_level = next_level - raise NodeNotFoundError('node missing at index {}'.format(index)) + raise NodeNotFoundError("node missing at index {}".format(index)) def __setitem__(self, index, node): """Insert a node (or subtree) at the given level-order_ index. @@ -705,14 +418,15 @@ def __setitem__(self, index, node): Node(4) """ if index == 0: - raise NodeModifyError('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) @@ -764,23 +478,73 @@ def __delitem__(self, index): NodeNotFoundError: node missing at index 2 """ if index == 0: - raise NodeModifyError('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)) + raise NodeNotFoundError("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)) + raise NodeNotFoundError("no node to delete at index {}".format(index)) setattr(parent, child_attr, None) - def pprint(self, index=False, delimiter='-'): + def _repr_svg_(self): + """Display the binary tree using Graphviz (used for `Jupyter notebooks`_). + + .. _Jupyter notebooks: https://jupyter.org + """ + # noinspection PyProtectedMember + return self.graphviz()._repr_svg_() + + def graphviz(self, *args, **kwargs) -> Digraph: + """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. + + :return: graphviz.Digraph_ object representing the binary tree. + + .. code-block:: python + + >>> from binarytree import tree + >>> + >>> t = tree() + >>> + >>> graph = t.graphviz() # Generate a graphviz object + >>> graph.body # Get the DOT body + >>> graph.render() # Render the graph + + .. _graphviz.Digraph: https://graphviz.readthedocs.io/en/stable/api.html#digraph + """ + if "node_attr" not in kwargs: + kwargs["node_attr"] = { + "shape": "record", + "style": "filled, rounded", + "color": "lightgray", + "fillcolor": "lightgray", + "fontcolor": "black", + } + + digraph = Digraph(*args, **kwargs) + + for node in self: + node_id = str(id(node)) + + digraph.node(node_id, nohtml(f"| {node.value}|")) + + if node.left is not None: + digraph.edge(f"{node_id}:l", f"{id(node.left)}:v") + + if node.right is not None: + digraph.edge(f"{node_id}:r", f"{id(node.right)}:v") + + return digraph + + def pprint(self, index=False, delimiter="-"): """Pretty-print the binary tree. :param index: If set to True (default: False), display level-order_ @@ -826,7 +590,7 @@ def pprint(self, index=False, delimiter='-'): 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))) + print("\n" + "\n".join((line.rstrip() for line in lines))) def validate(self): """Check if the binary tree is malformed. @@ -864,21 +628,33 @@ def validate(self): for node in to_visit: if node is None: - next_level.extend((None, None)) + next_level.append(None) + next_level.append(None) else: if node in visited: raise NodeReferenceError( - 'cyclic node reference at index {}'.format(index)) + f"cyclic reference at Node({node.val}) " + + f"(level-order index {index})" + ) if not isinstance(node, Node): raise NodeTypeError( - 'invalid node instance at index {}'.format(index)) - if not isinstance(node.val, numbers.Number): + "invalid node instance at index {}".format(index) + ) + if not isinstance(node.val, (float, int)): + raise NodeValueError( + "invalid node value at index {}".format(index) + ) + if not isinstance(node.value, (float, int)): raise NodeValueError( - 'invalid node value at index {}'.format(index)) + "invalid node value at index {}".format(index) + ) if node.left is not None or node.right is not None: has_more_nodes = True + visited.add(node) - next_level.extend((node.left, node.right)) + next_level.append(node.left) + next_level.append(node.right) + index += 1 to_visit = next_level @@ -922,14 +698,16 @@ def values(self): for node in current_level: if node is None: values.append(None) - next_level.extend((None, None)) + next_level.append(None) + next_level.append(None) continue if node.left is not None or node.right is not None: has_more_nodes = True values.append(node.val) - next_level.extend((node.left, node.right)) + next_level.append(node.left) + next_level.append(node.right) current_level = next_level @@ -1065,7 +843,7 @@ def height(self): .. note:: A binary tree with only a root node has a height of 0. """ - return _get_tree_properties(self)['height'] + return _get_tree_properties(self)["height"] @property def size(self): @@ -1091,7 +869,7 @@ def size(self): .. note:: This method is equivalent to :func:`binarytree.Node.__len__`. """ - return _get_tree_properties(self)['size'] + return _get_tree_properties(self)["size"] @property def leaf_count(self): @@ -1116,7 +894,7 @@ def leaf_count(self): >>> root.leaf_count 2 """ - return _get_tree_properties(self)['leaf_count'] + return _get_tree_properties(self)["leaf_count"] @property def is_balanced(self): @@ -1184,7 +962,7 @@ def is_bst(self): >>> root.is_bst True """ - return _is_bst(self, float('-inf'), float('inf')) + return _is_bst(self) @property def is_symmetric(self): @@ -1252,7 +1030,7 @@ def is_max_heap(self): >>> root.is_max_heap True """ - return _get_tree_properties(self)['is_max_heap'] + return _get_tree_properties(self)["is_max_heap"] @property def is_min_heap(self): @@ -1282,7 +1060,7 @@ def is_min_heap(self): >>> root.is_min_heap True """ - return _get_tree_properties(self)['is_min_heap'] + return _get_tree_properties(self)["is_min_heap"] @property def is_perfect(self): @@ -1319,7 +1097,7 @@ def is_perfect(self): >>> root.is_perfect True """ - return _get_tree_properties(self)['is_perfect'] + return _get_tree_properties(self)["is_perfect"] @property def is_strict(self): @@ -1354,7 +1132,7 @@ def is_strict(self): >>> root.is_strict True """ - return _get_tree_properties(self)['is_strict'] + return _get_tree_properties(self)["is_strict"] @property def is_complete(self): @@ -1391,7 +1169,7 @@ def is_complete(self): >>> root.is_complete True """ - return _get_tree_properties(self)['is_complete'] + return _get_tree_properties(self)["is_complete"] @property def min_node_value(self): @@ -1413,7 +1191,7 @@ def min_node_value(self): >>> root.min_node_value 1 """ - return _get_tree_properties(self)['min_node_value'] + return _get_tree_properties(self)["min_node_value"] @property def max_node_value(self): @@ -1435,7 +1213,7 @@ def max_node_value(self): >>> root.max_node_value 3 """ - return _get_tree_properties(self)['max_node_value'] + return _get_tree_properties(self)["max_node_value"] @property def max_leaf_depth(self): @@ -1469,7 +1247,7 @@ def max_leaf_depth(self): >>> root.max_leaf_depth 3 """ - return _get_tree_properties(self)['max_leaf_depth'] + return _get_tree_properties(self)["max_leaf_depth"] @property def min_leaf_depth(self): @@ -1503,7 +1281,7 @@ def min_leaf_depth(self): >>> root.min_leaf_depth 1 """ - return _get_tree_properties(self)['min_leaf_depth'] + return _get_tree_properties(self)["min_leaf_depth"] @property def properties(self): @@ -1557,11 +1335,13 @@ def properties(self): True """ properties = _get_tree_properties(self) - properties.update({ - 'is_bst': _is_bst(self), - 'is_balanced': _is_balanced(self) >= 0, - 'is_symmetric': _is_symmetric(self) - }) + properties.update( + { + "is_bst": _is_bst(self), + "is_balanced": _is_balanced(self) >= 0, + "is_symmetric": _is_symmetric(self), + } + ) return properties @property @@ -1598,8 +1378,8 @@ def inorder(self): >>> root.inorder [Node(4), Node(2), Node(5), Node(1), Node(3)] """ - result = [] - stack = [] + result: List[Node] = [] + stack: List[Node] = [] node = self while node or stack: @@ -1756,6 +1536,310 @@ def levelorder(self): return result +def _is_balanced(root: Optional[Node]): + """Return the tree height + 1 if balanced, -1 otherwise. + + :param root: Root node of the binary tree. + :type root: binarytree.Node + :return: Height if the binary tree is balanced, -1 otherwise. + :rtype: int + """ + if root is None: + return 0 + left = _is_balanced(root.left) + if left < 0: + return -1 + right = _is_balanced(root.right) + if right < 0: + return -1 + return -1 if abs(left - right) > 1 else max(left, right) + 1 + + +def _is_bst(root: Node): + """Check if the binary tree is a BST (binary search tree). + + :param root: Root node of the binary tree. + :type root: binarytree.Node + :return: True if the binary tree is a BST, False otherwise. + :rtype: bool + """ + stack: List[Node] = [] + cur: Optional[Node] = root + pre: Optional[Node] = None + while stack or cur is not None: + if cur is not None: + stack.append(cur) + cur = cur.left + else: + node = stack.pop() + if pre is not None and node.val <= pre.val: + return False + pre = node + cur = node.right + return True + + +def _is_symmetric(root): + """Check if the binary tree is symmetric. + + :param root: Root node of the binary tree. + :type root: binarytree.Node + :return: True if the binary tree is symmetric, False otherwise. + :rtype: bool + """ + + def symmetric_helper(left_subtree, right_subtree): + if left_subtree is None and right_subtree is None: + return True + if left_subtree is None or right_subtree is None: + return False + return ( + left_subtree.val == right_subtree.val + and symmetric_helper(left_subtree.left, right_subtree.right) + and symmetric_helper(left_subtree.right, right_subtree.left) + ) + + return symmetric_helper(root, root) + + +def _validate_tree_height(height): + """Check if the height of the binary tree is valid. + + :param height: Height of the binary tree (must be 0 - 9 inclusive). + :type height: int + :raise binarytree.exceptions.TreeHeightError: If height is invalid. + """ + if not (isinstance(height, int) and 0 <= height <= 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. + + :param height: Height of the BST. + :type height: int + :return: Root node of the BST. + :rtype: binarytree.Node + """ + max_node_count = 2 ** (height + 1) - 1 + node_values = list(range(max_node_count)) + return _build_bst_from_sorted_values(node_values) + + +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 | float] + :return: Root node of the BST. + :rtype: binarytree.Node + """ + if len(sorted_values) == 0: + return None + mid_index = len(sorted_values) // 2 + root = Node(sorted_values[mid_index]) + root.left = _build_bst_from_sorted_values(sorted_values[:mid_index]) + root.right = _build_bst_from_sorted_values(sorted_values[mid_index + 1 :]) + return root + + +def _generate_random_leaf_count(height): + """Return a random leaf count for building binary trees. + + :param height: Height of the binary tree. + :type height: int + :return: Random leaf count. + :rtype: int + """ + max_leaf_count = 2 ** height + half_leaf_count = max_leaf_count // 2 + + # A very naive way of mimicking normal distribution + roll_1 = random.randint(0, half_leaf_count) + roll_2 = random.randint(0, max_leaf_count - half_leaf_count) + return roll_1 + roll_2 or half_leaf_count + + +def _generate_random_node_values(height): + """Return random node values for building binary trees. + + :param height: Height of the binary tree. + :type height: int + :return: Randomly generated node values. + :rtype: [int] + """ + max_node_count = 2 ** (height + 1) - 1 + node_values = list(range(max_node_count)) + random.shuffle(node_values) + return node_values + + +def _build_tree_string(root, curr_index, index=False, delimiter="-"): + """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 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: Root node of the binary tree. + :type root: binarytree.Node + :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). + :type index: bool + :param delimiter: Delimiter character between the node index and the node + value (default: '-'). + :type delimiter: + :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#Breadth-first_search + """ + if root is None: + return [], 0, 0, 0 + + line1 = [] + line2 = [] + if index: + node_repr = "{}{}{}".format(curr_index, delimiter, root.val) + else: + node_repr = str(root.val) + + new_root_width = gap_size = len(node_repr) + + # Get the left and right sub-boxes, their widths, and root repr positions + l_box, l_box_width, l_root_start, l_root_end = _build_tree_string( + root.left, 2 * curr_index + 1, index, 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 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)) + line1.append("_" * (l_box_width - l_root)) + line2.append(" " * l_root + "/") + line2.append(" " * (l_box_width - l_root)) + new_root_start = l_box_width + 1 + gap_size += 1 + else: + new_root_start = 0 + + # Draw the representation of the current root node + line1.append(node_repr) + line2.append(" " * new_root_width) + + # 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) + line1.append(" " * (r_box_width - r_root + 1)) + line2.append(" " * r_root + "\\") + line2.append(" " * (r_box_width - r_root)) + gap_size += 1 + new_root_end = new_root_start + new_root_width - 1 + + # Combine the left and right sub-boxes with the branches drawn above + gap = " " * gap_size + new_box = ["".join(line1), "".join(line2)] + for i in range(max(len(l_box), len(r_box))): + l_line = l_box[i] if i < len(l_box) else " " * l_box_width + 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 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: Root node of the binary tree. + :type root: binarytree.Node + :return: Binary tree properties. + :rtype: dict + """ + is_descending = True + is_ascending = True + min_node_value = root.val + max_node_value = root.val + size = 0 + leaf_count = 0 + min_leaf_depth = 0 + max_leaf_depth = -1 + is_strict = True + is_complete = True + current_level = [root] + non_full_node_seen = False + + while len(current_level) > 0: + max_leaf_depth += 1 + next_level = [] + + for node in current_level: + size += 1 + val = node.val + min_node_value = min(val, min_node_value) + max_node_value = max(val, max_node_value) + + # 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 + leaf_count += 1 + + if node.left is not None: + if node.left.val > val: + is_descending = False + elif node.left.val < val: + is_ascending = False + next_level.append(node.left) + is_complete = not non_full_node_seen + else: + non_full_node_seen = True + + if node.right is not None: + if node.right.val > val: + is_descending = False + elif node.right.val < val: + is_ascending = False + next_level.append(node.right) + is_complete = not non_full_node_seen + else: + non_full_node_seen = True + + # If we see a node with only one child, it is not strict + is_strict &= (node.left is None) == (node.right is None) + + current_level = next_level + + return { + "height": max_leaf_depth, + "size": size, + "is_max_heap": is_complete and is_descending, + "is_min_heap": is_complete and is_ascending, + "is_perfect": leaf_count == 2 ** max_leaf_depth, + "is_strict": is_strict, + "is_complete": is_complete, + "leaf_count": leaf_count, + "min_node_value": min_node_value, + "max_node_value": max_node_value, + "min_leaf_depth": min_leaf_depth, + "max_leaf_depth": max_leaf_depth, + } + + def build(values): """Build a tree from `list representation`_ and return its root node. @@ -1808,7 +1892,8 @@ 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 @@ -1864,11 +1949,11 @@ def tree(height=3, is_perfect=False): return build(values) leaf_count = _generate_random_leaf_count(height) - root = Node(values.pop(0)) + root_node = Node(values.pop(0)) leaves = set() for value in values: - node = root + node = root_node depth = 0 inserted = False @@ -1885,7 +1970,7 @@ def tree(height=3, is_perfect=False): if len(leaves) == leaf_count: break - return root + return root_node def bst(height=3, is_perfect=False): @@ -1930,11 +2015,11 @@ def bst(height=3, is_perfect=False): values = _generate_random_node_values(height) leaf_count = _generate_random_leaf_count(height) - root = Node(values.pop(0)) + root_node = Node(values.pop(0)) leaves = set() for value in values: - node = root + node = root_node depth = 0 inserted = False @@ -1951,7 +2036,7 @@ def bst(height=3, is_perfect=False): if len(leaves) == leaf_count: break - return root + return root_node def heap(height=3, is_max=True, is_perfect=False): diff --git a/binarytree/exceptions.py b/binarytree/exceptions.py index 6478d4e..0676e24 100644 --- a/binarytree/exceptions.py +++ b/binarytree/exceptions.py @@ -1,6 +1,3 @@ -from __future__ import absolute_import, unicode_literals - - class BinaryTreeError(Exception): """Base (catch-all) binarytree exception.""" diff --git a/binarytree/version.py b/binarytree/version.py deleted file mode 100644 index aa96767..0000000 --- a/binarytree/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '5.1.0' # pragma: no cover diff --git a/docs/Makefile b/docs/Makefile index 6a19874..d4bb2cb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,10 +1,10 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = binarytree +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 707dfc6..669c0d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,177 +1,60 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# binarytree documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 9 02:56:43 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # 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. # +import os +import sys -_version = {} -with open("../binarytree/version.py") as fp: - exec(fp.read(), _version) +sys.path.insert(0, os.path.abspath("..")) -# -- General configuration ------------------------------------------------ +import binarytree -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' +# -- Project information ----------------------------------------------------- + +project = "binarytree" +copyright = "2016,2017,2018,2019,2020,2021, Joohwan Oh" +author = "Joohwan Oh" + + +# -- General configuration --------------------------------------------------- # 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.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages' + "sphinx_rtd_theme", + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", ] -# Add any paths that contain templates here, relative to this directory. -templates_path = [] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'binarytree' -copyright = '2016, Joohwan Oh' -author = 'Joohwan Oh' +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# -# The short X.Y version. -version = _version['__version__'] -# The full version, including alpha/beta/rc tags. -release = _version['__version__'] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False +version = binarytree.__version__ +release = binarytree.__version__ +# The master toctree document. +master_doc = "index" -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'binarytreedoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'binarytree.tex', 'binarytree Documentation', - 'Joohwan Oh', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'binarytree', 'binarytree Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'binarytree', 'binarytree Documentation', - author, 'binarytree', 'One line description of project.', - 'Miscellaneous'), -] +html_theme = "sphinx_rtd_theme" -autodoc_member_order = 'bysource' +htmlhelp_basename = "binarytreedoc" diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index b3bbabd..0000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,77 +0,0 @@ -Contributing ------------- - -Requirements -============ - -When submitting a pull request, please ensure your changes meet the following -requirements: - -* Pull request points to dev_ (development) branch. -* Changes are squashed into a single commit. -* Commit message is in present tense (e.g. "Add foo" over "Added foo"). -* Sphinx_-compatible docstrings. -* PEP8_ compliance. -* Test coverage_ remains at %100. -* No build failures on TravisCI_. -* Up-to-date documentation (see below). -* Maintains backward-compatibility. -* Maintains compatibility with Python 2.7+ and 3.4+. - -Style -===== - -Run flake8_ to check style: - -.. code-block:: bash - - ~$ pip install flake8 - ~$ git clone https://github.com/joowani/binarytree.git - ~$ cd binarytree - ~$ flake8 - - -Testing -======= - -Run unit tests: - -.. code-block:: bash - - ~$ pip install pytest - ~$ git clone https://github.com/joowani/binarytree.git - ~$ cd binarytree - ~$ py.test --verbose - -Run unit tests with coverage: - -.. code-block:: bash - - ~$ pip install coverage pytest pytest-cov - ~$ git clone https://github.com/joowani/binarytree.git - ~$ cd binarytree - ~$ py.test --cov=binarytree --cov-report=html - - # Open the generated file htmlcov/index.html in a browser - -Documentation -============= - -Documentation uses reStructuredText_ and Sphinx_. To build locally: - -.. code-block:: bash - - ~$ pip install sphinx sphinx_rtd_theme - ~$ git clone https://github.com/joowani/binarytree.git - ~$ cd binarytree/docs - ~$ sphinx-build . build - # Open build/index.html in a browser - - -.. _dev: https://github.com/joowani/binarytree/tree/dev -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ -.. _coverage: https://coveralls.io/github/joowani/binarytree -.. _TravisCI: https://travis-ci.org/joowani/binarytree -.. _Sphinx: https://github.com/sphinx-doc/sphinx -.. _flake8: http://flake8.pycqa.org -.. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText diff --git a/docs/graphviz.rst b/docs/graphviz.rst new file mode 100644 index 0000000..8b66200 --- /dev/null +++ b/docs/graphviz.rst @@ -0,0 +1,49 @@ +Graphviz and Jupyter Notebook +----------------------------- + +From version 6.0.0, binarytree can integrate with Graphviz_ to render trees in image +viewers, browsers and Jupyter notebooks using the python-graphviz_ library. + +In order to use this feature, you must first install the Graphviz software in your OS +and ensure its executables are on your PATH system variable (usually done automatically +during installation): + +.. code-block:: bash + + # Ubuntu and Debian + $ sudo apt install graphviz + + # Fedora and CentOS + $ sudo yum install graphviz + + # Windows using choco (or winget) + $ choco install graphviz + +Use :func:`binarytree.Node.graphviz` to generate `graphviz.Digraph`_ objects: + +.. code-block:: python + + from binarytree import tree + + t = tree() + + # Generate a graphviz.Digraph object + # Arguments to this method are passed into Digraph.__init__ + graph = t.graphviz() + + # Get DOT (graph description language) body + graph.body + + # Render the binary tree + graph.render() + +With Graphviz you can also visualize binary trees in `Jupyter notebooks`_: + +.. image:: https://user-images.githubusercontent.com/2701938/107016813-3c818600-6753-11eb-8140-6b7a95791c08.gif + :alt: Jupyter Notebook GIF + +.. _DOT: https://graphviz.org/doc/info/lang.html +.. _Graphviz: https://graphviz.org/ +.. _python-graphviz: https://github.com/xflr6/graphviz +.. _graphviz.Digraph: https://graphviz.readthedocs.io/en/stable/api.html#digraph +.. _Jupyter notebooks: https://jupyter.org/ diff --git a/docs/index.rst b/docs/index.rst index 3396e1b..52559ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,36 +3,33 @@ Binarytree 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. +**Binarytree** is Python library which lets you generate, visualize, inspect and +manipulate binary trees. Skip the tedious work of setting up test data, and dive +straight into practising algorithms! Heaps and BSTs (binary search trees) are also +supported. Requirements ============ -- Python 2.7+ or 3.4+ +Python 3.6+ Installation ============ -To install a stable version from PyPi_: +Install via pip_: .. code-block:: bash - ~$ pip install binarytree + pip install binarytree - -To install the latest version directly from GitHub_: +For conda_ users: .. 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. + conda install binarytree -c conda-forge -.. _PyPi: https://pypi.python.org/pypi/binarytree -.. _GitHub: https://github.com/joowani/binarytree +.. _pip: https://pip.pypa.io +.. _conda: https://docs.conda.io Contents ======== @@ -43,4 +40,4 @@ Contents overview specs exceptions - contributing + graphviz diff --git a/docs/make.bat b/docs/make.bat index cd90edd..922152e 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,32 +5,31 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build -set SPHINXPROJ=binarytree if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd diff --git a/docs/overview.rst b/docs/overview.rst index 8de6bf8..1195642 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -1,14 +1,14 @@ Overview -------- -By default, **binarytree** uses the following class to represent a node: +Binarytree uses the following class to represent a node: .. testcode:: - class Node(object): + class Node: def __init__(self, value, left=None, right=None): - self.val = value # The node value + self.value = value # The node value (integer) self.left = left # Left child self.right = right # Right child @@ -59,7 +59,7 @@ Generate and pretty-print various types of binary trees: 0 10 6 -Use the :class:`binarytree.Node` class to build your own trees: +Build your own trees: .. doctest:: @@ -203,7 +203,7 @@ Use `level-order (breadth-first)`_ indexes to manipulate nodes: 2-3 -Traverse the trees using different algorithms: +Traverse trees using different algorithms: .. doctest:: diff --git a/demo.gif b/gifs/demo.gif similarity index 100% rename from demo.gif rename to gifs/demo.gif diff --git a/gifs/jupyter.gif b/gifs/jupyter.gif new file mode 100644 index 0000000..b8a4cfd Binary files /dev/null and b/gifs/jupyter.gif differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3fba808 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "setuptools_scm[toml]>=3.4" +] +build-backend = "setuptools.build_meta" + +[tool.coverage.run] +omit = ["binarytree/version.py"] + +[tool.isort] +profile = "black" + +[tool.pytest.ini_options] +addopts = "-s -vv -p no:warnings" +minversion = "6.0" +testpaths = ["tests"] + +[tool.setuptools_scm] +write_to = "binarytree/version.py" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index e3d24e5..0000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -python_files = tests.py test_*.py *_tests.py -addopts = -s -vv -p no:warnings --cov=binarytree -norecursedirs = .cache .git .idea .pytest_cache build dist docs htmlcov venv diff --git a/setup.cfg b/setup.cfg index e78e0ec..32a3ba6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,8 @@ -[bdist_wheel] -universal = 1 - [flake8] -exclude = .cache,.git,.idea,.pytest_cache,__pycache__,docs/conf.py,dist,venv +max-line-length = 88 +extend-ignore = E203, E741, W503 +exclude =.git .idea .*_cache dist htmlcov venv +per-file-ignores = __init__.py:F401 conf.py:E402 + +[mypy] +ignore_missing_imports = True diff --git a/setup.py b/setup.py index 0cb9071..739dbec 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,49 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup -version = {} -with open("./binarytree/version.py") as fp: - exec(fp.read(), version) - -with open('./README.rst') as fp: +with open("README.md") as fp: description = fp.read() setup( - name='binarytree', - description='Python Library for Studying Binary Trees', + name="binarytree", + description="Python Library for Studying Binary Trees", long_description=description, - version=version['__version__'], - author='Joohwan Oh', - author_email='joohwan.oh@outlook.com', - url='https://github.com/joowani/binarytree', - packages=find_packages(exclude=['tests']), + long_description_content_type="text/markdown", + author="Joohwan Oh", + author_email="joohwan.oh@outlook.com", + url="https://github.com/joowani/binarytree", + packages=find_packages(exclude=["tests"]), include_package_data=True, - tests_require=['pytest', 'flake8'], - license='MIT', + python_requires=">=3.6", + license="MIT", + use_scm_version=True, + setup_requires=["setuptools_scm"], + install_requires=[ + "graphviz", + "setuptools>=42", + "setuptools_scm[toml]>=3.4", + ], + extras_require={ + "dev": [ + "black", + "flake8", + "isort>=5.0.0", + "mypy", + "pre-commit", + "pytest>=6.0.0", + "pytest-cov>=2.0.0", + "sphinx", + "sphinx_rtd_theme", + ], + }, classifiers=[ - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Education' - ] + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Education", + ], ) diff --git a/tests/test_tree.py b/tests/test_tree.py index 23e7756..765169b 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -5,22 +5,17 @@ import pytest -from binarytree import Node, build, tree, bst, heap -from binarytree import get_parent +from binarytree import Node, bst, build, get_parent, heap, tree from binarytree.exceptions import ( - NodeValueError, NodeIndexError, - NodeTypeError, NodeModifyError, NodeNotFoundError, - TreeHeightError, NodeReferenceError, + NodeTypeError, + NodeValueError, + TreeHeightError, ) -from tests.utils import ( - builtin_print, - pprint_default, - pprint_with_index -) +from tests.utils import builtin_print, pprint_default, pprint_with_index REPETITIONS = 20 @@ -31,17 +26,17 @@ def test_node_set_attributes(): assert root.right is None assert root.val == 1 assert root.value == 1 - assert repr(root) == 'Node(1)' + assert repr(root) == "Node(1)" root.value = 2 assert root.value == 2 assert root.val == 2 - assert repr(root) == 'Node(2)' + assert repr(root) == "Node(2)" root.val = 1 assert root.value == 1 assert root.val == 1 - assert repr(root) == 'Node(1)' + assert repr(root) == "Node(1)" left_child = Node(2) root.left = left_child @@ -51,7 +46,7 @@ def test_node_set_attributes(): assert root.left.left is None assert root.left.right is None assert root.left.val == 2 - assert repr(left_child) == 'Node(2)' + assert repr(left_child) == "Node(2)" right_child = Node(3) root.right = right_child @@ -61,47 +56,47 @@ def test_node_set_attributes(): assert root.right.left is None assert root.right.right is None assert root.right.val == 3 - assert repr(right_child) == 'Node(3)' + assert repr(right_child) == "Node(3)" last_node = Node(4) left_child.right = last_node assert root.left.right is last_node - assert repr(root.left.right) == 'Node(4)' + 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 number' + 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' + Node(1, "this_is_not_a_node") + assert str(err.value) == "left child must be a Node instance" 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' + Node(1, Node(1), "this_is_not_a_node") + assert str(err.value) == "right child must be a Node instance" with pytest.raises(NodeValueError) as err: - root.val = 'this_is_not_an_integer' + root.val = "this_is_not_an_integer" assert root.val == 1 - assert str(err.value) == 'node value must be a number' + assert str(err.value) == "node value must be a float or int" with pytest.raises(NodeValueError) as err: - root.value = 'this_is_not_an_integer' + root.value = "this_is_not_an_integer" assert root.value == 1 - assert str(err.value) == 'node value must be a number' + assert str(err.value) == "node value must be a float or int" with pytest.raises(NodeTypeError) as err: - root.left = 'this_is_not_a_node' + root.left = "this_is_not_a_node" assert root.left is left_child - assert str(err.value) == 'left child must be a Node instance' + assert str(err.value) == "left child must be a Node instance" with pytest.raises(NodeTypeError) as err: - root.right = 'this_is_not_a_node' + root.right = "this_is_not_a_node" assert root.right is right_child - assert str(err.value) == 'right child must be a Node instance' + assert str(err.value) == "right child must be a Node instance" # noinspection PyUnresolvedReferences @@ -141,11 +136,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" def test_tree_get_node(): @@ -166,11 +161,11 @@ def test_tree_get_node(): for index in [5, 6, 7, 8, 10]: with pytest.raises(NodeNotFoundError) as err: assert root[index] - assert str(err.value) == 'node missing at index {}'.format(index) + assert str(err.value) == "node missing at index {}".format(index) with pytest.raises(NodeIndexError) as err: assert root[-1] - assert str(err.value) == 'node index must be a non-negative int' + assert str(err.value) == "node index must be a non-negative int" def test_tree_set_node(): @@ -187,15 +182,15 @@ def test_tree_set_node(): 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(NodeIndexError) as err: root[-1] = new_node_1 - assert str(err.value) == 'node index must be a non-negative int' + 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.val == 1 @@ -231,19 +226,19 @@ def test_tree_del_node(): 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(NodeIndexError) as err: del root[-1] - assert str(err.value) == 'node index must be a non-negative int' + 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 @@ -284,88 +279,79 @@ def test_tree_del_node(): def test_tree_print_no_index(): for printer in [builtin_print, pprint_default]: lines = printer([1]) - assert lines == ['1'] + assert lines == ["1"] lines = printer([1, 2]) - assert lines == [' 1', - ' /', - '2'] + assert lines == [" 1", " /", "2"] lines = printer([1, None, 3]) - assert lines == ['1', - ' \\', - ' 3'] + assert lines == ["1", " \\", " 3"] lines = printer([1, 2, 3]) - assert lines == [' 1', - ' / \\', - '2 3'] + assert lines == [" 1", " / \\", "2 3"] lines = printer([1, 2, 3, None, 5]) - assert lines == [' __1', - ' / \\', - '2 3', - ' \\', - ' 5'] + assert lines == [" __1", " / \\", "2 3", " \\", " 5"] lines = printer([1, 2, 3, None, 5, 6]) - assert lines == [' __1__', - ' / \\', - '2 3', - ' \\ /', - ' 5 6'] + assert lines == [" __1__", " / \\", "2 3", " \\ /", " 5 6"] lines = printer([1, 2, 3, None, 5, 6, 7]) - assert lines == [' __1__', - ' / \\', - '2 3', - ' \\ / \\', - ' 5 6 7'] + assert lines == [ + " __1__", + " / \\", + "2 3", + " \\ / \\", + " 5 6 7", + ] lines = printer([1, 2, 3, 8, 5, 6, 7]) - assert lines == [' __1__', - ' / \\', - ' 2 3', - ' / \\ / \\', - '8 5 6 7'] + assert lines == [ + " __1__", + " / \\", + " 2 3", + " / \\ / \\", + "8 5 6 7", + ] def test_tree_print_with_index(): lines = pprint_with_index([1]) - assert lines == ['0:1'] + assert lines == ["0:1"] lines = pprint_with_index([1, 2]) - assert lines == [' _0:1', - ' /', - '1:2'] + assert lines == [" _0:1", " /", "1:2"] lines = pprint_with_index([1, None, 3]) - assert lines == ['0:1_', - ' \\', - ' 2:3'] + assert lines == ["0:1_", " \\", " 2:3"] lines = pprint_with_index([1, 2, 3]) - assert lines == [' _0:1_', - ' / \\', - '1:2 2:3'] + assert lines == [" _0:1_", " / \\", "1:2 2:3"] lines = pprint_with_index([1, 2, 3, None, 5]) - assert lines == [' _____0:1_', - ' / \\', - '1:2_ 2:3', - ' \\', - ' 4:5'] + assert lines == [ + " _____0:1_", + " / \\", + "1:2_ 2:3", + " \\", + " 4:5", + ] lines = pprint_with_index([1, 2, 3, None, 5, 6]) - assert lines == [' _____0:1_____', - ' / \\', - '1:2_ _2:3', - ' \\ /', - ' 4:5 5:6'] + assert lines == [ + " _____0:1_____", + " / \\", + "1:2_ _2:3", + " \\ /", + " 4:5 5:6", + ] 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'] + assert lines == [ + " _____0:1_____", + " / \\", + "1:2_ _2:3_", + " \\ / \\", + " 4:5 5:6 6:7", + ] lines = pprint_with_index([1, 2, 3, 8, 5, 6, 7]) - assert lines == [' _____0:1_____', - ' / \\', - ' _1:2_ _2:3_', - ' / \\ / \\', - '3:8 4:5 5:6 6:7'] + assert lines == [ + " _____0:1_____", + " / \\", + " _1:2_ _2:3_", + " / \\ / \\", + "3:8 4:5 5:6 6:7", + ] def test_tree_validate(): - class TestNode(Node): def __setattr__(self, attr, value): object.__setattr__(self, attr, value) @@ -391,44 +377,44 @@ def __setattr__(self, attr, value): root.validate() # Should pass root = TestNode(1) - root.left = 'not_a_node' + root.left = "not_a_node" 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.val = 'not_an_integer' + root.right.val = "not_an_integer" 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(NodeReferenceError) as err: root.validate() - assert str(err.value) == 'cyclic node reference at index 4' + assert str(err.value) == "cyclic reference at Node(1) (level-order index 4)" def test_tree_properties(): root = Node(1) assert root.properties == { - 'height': 0, - 'is_balanced': True, - 'is_bst': True, - 'is_complete': True, - 'is_max_heap': True, - 'is_min_heap': True, - 'is_perfect': True, - 'is_strict': True, - 'is_symmetric': True, - 'leaf_count': 1, - 'max_leaf_depth': 0, - 'max_node_value': 1, - 'min_leaf_depth': 0, - 'min_node_value': 1, - 'size': 1 + "height": 0, + "is_balanced": True, + "is_bst": True, + "is_complete": True, + "is_max_heap": True, + "is_min_heap": True, + "is_perfect": True, + "is_strict": True, + "is_symmetric": True, + "leaf_count": 1, + "max_leaf_depth": 0, + "max_node_value": 1, + "min_leaf_depth": 0, + "min_node_value": 1, + "size": 1, } assert root.height == 0 assert root.is_balanced is True @@ -448,21 +434,21 @@ def test_tree_properties(): root.left = Node(2) assert root.properties == { - 'height': 1, - 'is_balanced': True, - 'is_bst': False, - 'is_complete': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_perfect': False, - 'is_strict': False, - 'is_symmetric': False, - 'leaf_count': 1, - 'max_leaf_depth': 1, - 'max_node_value': 2, - 'min_leaf_depth': 1, - 'min_node_value': 1, - 'size': 2 + "height": 1, + "is_balanced": True, + "is_bst": False, + "is_complete": True, + "is_max_heap": False, + "is_min_heap": True, + "is_perfect": False, + "is_strict": False, + "is_symmetric": False, + "leaf_count": 1, + "max_leaf_depth": 1, + "max_node_value": 2, + "min_leaf_depth": 1, + "min_node_value": 1, + "size": 2, } assert root.height == 1 assert root.is_balanced is True @@ -482,21 +468,21 @@ def test_tree_properties(): root.right = Node(3) assert root.properties == { - 'height': 1, - 'is_balanced': True, - 'is_bst': False, - 'is_complete': True, - 'is_max_heap': False, - 'is_min_heap': True, - 'is_perfect': True, - 'is_strict': True, - 'is_symmetric': False, - 'leaf_count': 2, - 'max_leaf_depth': 1, - 'max_node_value': 3, - 'min_leaf_depth': 1, - 'min_node_value': 1, - 'size': 3 + "height": 1, + "is_balanced": True, + "is_bst": False, + "is_complete": True, + "is_max_heap": False, + "is_min_heap": True, + "is_perfect": True, + "is_strict": True, + "is_symmetric": False, + "leaf_count": 2, + "max_leaf_depth": 1, + "max_node_value": 3, + "min_leaf_depth": 1, + "min_node_value": 1, + "size": 3, } assert root.height == 1 assert root.is_balanced is True @@ -516,21 +502,21 @@ def test_tree_properties(): root.left.left = Node(4) assert 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': False, - 'is_symmetric': False, - 'leaf_count': 2, - 'max_leaf_depth': 2, - 'max_node_value': 4, - 'min_leaf_depth': 1, - 'min_node_value': 1, - 'size': 4 + "height": 2, + "is_balanced": True, + "is_bst": False, + "is_complete": True, + "is_max_heap": False, + "is_min_heap": True, + "is_perfect": False, + "is_strict": False, + "is_symmetric": False, + "leaf_count": 2, + "max_leaf_depth": 2, + "max_node_value": 4, + "min_leaf_depth": 1, + "min_node_value": 1, + "size": 4, } assert root.height == 2 assert root.is_balanced is True @@ -550,21 +536,21 @@ def test_tree_properties(): root.right.left = Node(5) assert root.properties == { - 'height': 2, - 'is_balanced': True, - 'is_bst': False, - 'is_complete': False, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_perfect': False, - 'is_strict': False, - 'is_symmetric': False, - 'leaf_count': 2, - 'max_leaf_depth': 2, - 'max_node_value': 5, - 'min_leaf_depth': 2, - 'min_node_value': 1, - 'size': 5 + "height": 2, + "is_balanced": True, + "is_bst": False, + "is_complete": False, + "is_max_heap": False, + "is_min_heap": False, + "is_perfect": False, + "is_strict": False, + "is_symmetric": False, + "leaf_count": 2, + "max_leaf_depth": 2, + "max_node_value": 5, + "min_leaf_depth": 2, + "min_node_value": 1, + "size": 5, } assert root.height == 2 assert root.is_balanced is True @@ -584,21 +570,21 @@ def test_tree_properties(): root.right.left.left = Node(6) assert root.properties == { - 'height': 3, - 'is_balanced': False, - 'is_bst': False, - 'is_complete': False, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_perfect': False, - 'is_strict': False, - 'is_symmetric': False, - 'leaf_count': 2, - 'max_leaf_depth': 3, - 'max_node_value': 6, - 'min_leaf_depth': 2, - 'min_node_value': 1, - 'size': 6 + "height": 3, + "is_balanced": False, + "is_bst": False, + "is_complete": False, + "is_max_heap": False, + "is_min_heap": False, + "is_perfect": False, + "is_strict": False, + "is_symmetric": False, + "leaf_count": 2, + "max_leaf_depth": 3, + "max_node_value": 6, + "min_leaf_depth": 2, + "min_node_value": 1, + "size": 6, } assert root.height == 3 assert root.is_balanced is False @@ -618,21 +604,21 @@ def test_tree_properties(): root.left.left.left = Node(7) assert root.properties == { - 'height': 3, - 'is_balanced': False, - 'is_bst': False, - 'is_complete': False, - 'is_max_heap': False, - 'is_min_heap': False, - 'is_perfect': False, - 'is_strict': False, - 'is_symmetric': False, - 'leaf_count': 2, - 'max_leaf_depth': 3, - 'max_node_value': 7, - 'min_leaf_depth': 3, - 'min_node_value': 1, - 'size': 7 + "height": 3, + "is_balanced": False, + "is_bst": False, + "is_complete": False, + "is_max_heap": False, + "is_min_heap": False, + "is_perfect": False, + "is_strict": False, + "is_symmetric": False, + "leaf_count": 2, + "max_leaf_depth": 3, + "max_node_value": 7, + "min_leaf_depth": 3, + "min_node_value": 1, + "size": 7, } assert root.height == 3 assert root.is_balanced is False @@ -715,10 +701,10 @@ def test_tree_list_representation(): def test_tree_generation(): - for invalid_height in ['foo', -1, None]: + for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: tree(height=invalid_height) - assert str(err.value) == 'height must be an int between 0 - 9' + assert str(err.value) == "height must be an int between 0 - 9" root = tree(height=0) root.validate() @@ -744,10 +730,10 @@ def test_tree_generation(): def test_bst_generation(): - for invalid_height in ['foo', -1, None]: + for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: bst(height=invalid_height) - assert str(err.value) == 'height must be an int between 0 - 9' + assert str(err.value) == "height must be an int between 0 - 9" root = bst(height=0) root.validate() @@ -771,7 +757,7 @@ def test_bst_generation(): if not root.is_bst: print(root) - raise Exception('boo') + raise Exception("boo") assert root.is_bst is True assert root.is_perfect is True @@ -780,10 +766,10 @@ def test_bst_generation(): def test_heap_generation(): - for invalid_height in ['foo', -1, None]: + for invalid_height in ["foo", -1, None]: with pytest.raises(TreeHeightError) as err: heap(height=invalid_height) - assert str(err.value) == 'height must be an int between 0 - 9' + assert str(err.value) == "height must be an int between 0 - 9" root = heap(height=0) root.validate() @@ -842,25 +828,21 @@ def test_heap_float_values(): for printer in [builtin_print, pprint_default]: lines = printer([1.0]) - assert lines == ['1.0'] + assert lines == ["1.0"] lines = printer([1.0, 2.0]) - assert lines == [' _1.0', - ' /', - '2.0'] + assert lines == [" _1.0", " /", "2.0"] lines = printer([1.0, None, 3.0]) - assert lines == ['1.0_', - ' \\', - ' 3.0'] + assert lines == ["1.0_", " \\", " 3.0"] lines = printer([1.0, 2.0, 3.0]) - assert lines == [' _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'] + assert lines == [ + " _____1.0_", + " / \\", + "2.0_ 3.0", + " \\", + " 5.0", + ] for builder in [tree, bst, heap]: for _ in range(REPETITIONS): diff --git a/tests/utils.py b/tests/utils.py index 9911bd2..1f5de92 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals import sys + try: # noinspection PyCompatibility from StringIO import StringIO @@ -29,18 +30,18 @@ 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 != ''] + 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 != ''] + root.pprint(index=True, delimiter=":") + assert output[0] == "" and output[-1] == "" + return [line for line in output if line != ""] def builtin_print(values): @@ -48,5 +49,5 @@ def builtin_print(values): root = build(values) with CaptureOutput() as output: print(root) - assert output[0] == '' and output[-1] == '' - return [line for line in output if line != ''] + assert output[0] == "" and output[-1] == "" + return [line for line in output if line != ""]