From 8ffbbd753d72221932be5bf404990aaa4a3c1b93 Mon Sep 17 00:00:00 2001 From: Joohwan Oh Date: Wed, 3 Feb 2021 02:30:27 -0800 Subject: [PATCH] Add changes for version 6.0.0 --- .github/workflows/build.yaml | 47 ++ .github/workflows/codeql.yaml | 22 + .github/workflows/pypi.yaml | 31 ++ .gitignore | 5 + .pre-commit-config.yaml | 31 ++ .travis.yml | 19 - LICENSE | 2 +- MANIFEST.in | 3 +- README.md | 315 +++++++++++++ README.rst | 352 -------------- binarytree/__init__.py | 845 +++++++++++++++++++--------------- binarytree/exceptions.py | 3 - binarytree/version.py | 1 - docs/Makefile | 10 +- docs/conf.py | 183 ++------ docs/contributing.rst | 77 ---- docs/graphviz.rst | 49 ++ docs/index.rst | 27 +- docs/make.bat | 15 +- docs/overview.rst | 10 +- demo.gif => gifs/demo.gif | Bin gifs/jupyter.gif | Bin 0 -> 55221 bytes pyproject.toml | 21 + pytest.ini | 4 - setup.cfg | 11 +- setup.py | 68 +-- tests/test_tree.py | 450 +++++++++--------- tests/utils.py | 17 +- 28 files changed, 1324 insertions(+), 1294 deletions(-) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/codeql.yaml create mode 100644 .github/workflows/pypi.yaml create mode 100644 .pre-commit-config.yaml delete mode 100644 .travis.yml create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 binarytree/version.py delete mode 100644 docs/contributing.rst create mode 100644 docs/graphviz.rst rename demo.gif => gifs/demo.gif (100%) create mode 100644 gifs/jupyter.gif create mode 100644 pyproject.toml delete mode 100644 pytest.ini 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 0000000000000000000000000000000000000000..b8a4cfda3d0254a8bb9b2fbb9e3ef981daaa87a3 GIT binary patch literal 55221 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 literal 0 HcmV?d00001 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 != ""]