commit 8626785ca353279f94141097f3f776d753fd8b81 Author: Luke Murphy Date: Thu Jun 20 17:12:38 2019 +0200 flat-tree implementation coming down the tubes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca15349 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.egg-info/ +*.pyc +.coverage +.eggs/ +.mypy_cache/ +.tox/ +.venv/ +__pycache__ +build/ +dist/ +pip-wheel-metadata/ +documentation/build/ diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..b796095 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,16 @@ +version: 2 + +build: + image: latest + +sphinx: + configuration: documentation/source/conf.py + fail_on_warning: true + +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..9e5dbd1 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,7 @@ +Flat_Tree 0.0.1a2 (2019-07-01) +============================== + +Project Announcements +--------------------- + +- Initial development release is made! (#1) diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 0000000..cb3f698 --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,111 @@ +Introduction +------------ + +The DatPy community is committed to providing an inclusive, safe, and +collaborative environment for all participants, regardless of their gender, +gender expression, race, ethnicity, religion, sexual orientation, sexual +characteristics, physical appearance, disability, or age. We encourage every +participant to be themselves, and must respect the rights of others. The code +of conduct is a set of guidelines that establishes shared values and ensures +that behaviors that may harm participants are avoided. + +The values of the DatPy community are focused on developing both our individual +and collective potential, supporting and empowering the most marginalized, +mutual respect, and an anti-violence approach that favors support and +collaboration among participants and the resolution of conflicts. A code of +conduct helps us co-exist in a more positive way and provides individuals who +are victims of negative behaviors with confidence that they will be supported +by the organization and the DatPy community, who respects and stands behind the +code of conduct. + +The DatPy community works towards providing a welcoming environment where +participants are treated with dignity and respect and are free to be +themselves. We encourage all participants to approach the Librehosters network +with an open and positive attitude, engaging constructively with others at all +times. + +Respect for Diversity & Inclusion +--------------------------------- + +We avoid comments, actions or propaganda that encourage discrimination related +to gender, gender expression, race, ethnicity, religion, sexual orientation, +sexual characteristics, physical appearance, disability, or age. + +Respect Freedom of Expression +----------------------------- + +We support an individual's freedom of expression, and will not make fun of +accents or make unsolicited grammatical corrections. We will strive to better +understand each other by not assuming experiences or beliefs, clarifying +meanings, and making an effort to speak clearly, avoiding jargon and acronyms. + +Commitment to Non-Violence +-------------------------- + +We will not engage in any type of violence or aggression, including verbal +threats or complaints, intimidation, stalking or harassment, whether physically +or psychologically. + +Rejection of Sexual Harassment +------------------------------ + +We understand sexual harassment as unwanted physical contact or insinuation of +a sexual nature, as well as displaying images, drawings or visual +representations of any kind that objectify members of any gender or reinforce +oppression. The only exception is if this is part of a session, workshop and/or +educational experience where showing these images is educational in nature. + +Respect for Privacy +------------------- + +We safeguard the privacy of the participants. This includes refraining from +posting or publishing information about attendees (including names and +affiliation) unless given clear permission, and avoid any type of unauthorized +video, audio recording, or photography. + +Facilitate Participation & Collaboration +---------------------------------------- + +We work to create an environment that facilitates participation for all +participants. We will not engage in sustained disruption of discussions or +events, interrupt conversations in a way that negatively impacts collaboration, +or engage in toxic behaviours to attract negative attention to a participant. + +We Care about the Integrity and Health of the Community +------------------------------------------------------- + +We value the health of the community and will not engage in behaviour that can +negatively impact it. This includes contaminating food or drink with drugs, or +inciting or insisting on the consumption of alcohol, psychoactive substances, +etc. + +Support Positive Interactions Among Participants +------------------------------------------------ + +We are committed to engaging constructively with others at all times. We will +not tolerate bullying, including requesting or mobilizing others, either in +person or online, to bully others. + +Enforcement +----------- + +Overseeing the code of conduct +============================== + +The DatPy community, composed of volunteers, oversees the code of conduct, +including addressing all incident reports. Breaking the code of conduct may +result in immediate expulsion from the Librehosters network. + +How to Report an Incident +========================= + +If you witness an incident or are the victim of one: + +1. You can reach out directly via email at ``lukewm [at] riseup.net``. + +Acknowledgements +---------------- + +This code of conduct is inspired by the [IFF CoC]. + +[IFF CoC]: https://www.internetfreedomfestival.org/wiki/index.php/Code_of_Conduct diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..2c15596 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,67 @@ +Get started +----------- + +Install `Tox`_ with: + +.. _tox: http://tox.readthedocs.io/ + +.. code-block:: bash + + $ pip install --user tox + +Run tests +--------- + +.. code-block:: bash + + tox -e test + +Lint source +----------- + +.. code-block:: bash + + tox -e lint + +Format source +------------- + +.. code-block:: bash + + tox -e format + +Type check source +----------------- + +.. code-block:: bash + + tox -e type + +Release Process +--------------- + +Add a change entry and re-generate the changelog: + +.. code-block:: bash + + $ towncrier + +Make a new release tag: + +.. code-block:: bash + + $ git tag x.x.x + $ git push --tags + +If you have a development install locally, you can verify: + +.. code-block:: bash + + $ flat_tree --version + +Then run the release process: + +.. code-block:: bash + + $ tox -e metadata-release + $ tox -e release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..56788e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Luke Murphy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2bc8bb4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE README.rst CHANGELOG.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..86aec12 --- /dev/null +++ b/README.rst @@ -0,0 +1,55 @@ +.. _header: + +********* +flat_tree +********* + +.. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg + :target: LICENSE + :alt: Repository license + +.. image:: https://badge.fury.io/py/flat_tree.svg + :target: https://badge.fury.io/py/flat_tree + :alt: PyPI Package + +.. image:: https://readthedocs.org/projects/flat-tree/badge/?version=latest + :target: https://flat-tree.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. image:: https://img.shields.io/badge/support-maintainers-brightgreen.svg + :target: https://decentral1.se/ + :alt: Support badge + +.. _introduction: + +Utilities for navigating flat trees +----------------------------------- + +From `The Dat Protocol`_: + +.. _The Dat Protocol: https://datprotocol.github.io/book/ch01-01-flat-tree.html + + Flat Trees are the core data structure that power Dat's Hypercore feeds. They + allow us to deterministically represent a tree structure as a vector. This is + particularly useful because vectors map elegantly to disk and memory. + + Because Flat Trees are deterministic and pre-computed, there is no overhead + to using them. In effect this means that Flat Trees are a specific way of + indexing into a vector more than they are their own data structure. This makes + them uniquely efficient and convenient to implement in a wide range of + languages. + +.. _documentation: + +Documentation +************* + +* https://flat_tree.readthedocs.io + +.. _mirroring: + +Mirroring +********* + +* https://hack.decentral1.se/datpy/flat_tree (primary) +* https://github.com/datpy/flat_tree diff --git a/documentation/Makefile b/documentation/Makefile new file mode 100644 index 0000000..a57cab8 --- /dev/null +++ b/documentation/Makefile @@ -0,0 +1,12 @@ +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = build + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/documentation/source/_static/.git-dont-delete b/documentation/source/_static/.git-dont-delete new file mode 100644 index 0000000..e69de29 diff --git a/documentation/source/_templates/.git-dont-delete b/documentation/source/_templates/.git-dont-delete new file mode 100644 index 0000000..e69de29 diff --git a/documentation/source/changelog.rst b/documentation/source/changelog.rst new file mode 100644 index 0000000..bed14d3 --- /dev/null +++ b/documentation/source/changelog.rst @@ -0,0 +1,5 @@ +********* +Changelog +********* + +.. include:: ../../CHANGELOG.rst diff --git a/documentation/source/code-of-conduct.rst b/documentation/source/code-of-conduct.rst new file mode 100644 index 0000000..3f04422 --- /dev/null +++ b/documentation/source/code-of-conduct.rst @@ -0,0 +1,6 @@ +.. _code-of-conduct: + +Code of Conduct +*************** + +.. include:: ../../CODE_OF_CONDUCT.rst diff --git a/documentation/source/conf.py b/documentation/source/conf.py new file mode 100644 index 0000000..19e02ef --- /dev/null +++ b/documentation/source/conf.py @@ -0,0 +1,8 @@ +author = 'decentral1se' +copyright = '2019, decentral1se' +html_static_path = ['_static'] +html_theme = 'alabaster' +master_doc = 'index' +project = 'flat_tree' +templates_path = ['_templates'] +extensions = ['sphinx.ext.autodoc', 'sphinx_autodoc_typehints'] diff --git a/documentation/source/contribute.rst b/documentation/source/contribute.rst new file mode 100644 index 0000000..0c99a3e --- /dev/null +++ b/documentation/source/contribute.rst @@ -0,0 +1,5 @@ +********** +Contribute +********** + +.. include:: ../../CONTRIBUTING.rst diff --git a/documentation/source/index.rst b/documentation/source/index.rst new file mode 100644 index 0000000..0a73afc --- /dev/null +++ b/documentation/source/index.rst @@ -0,0 +1,14 @@ +.. include:: ../../README.rst + :end-before: _documentation + +Table of Contents +***************** + +.. toctree:: + + install + modules-api + other-impls + contribute + changelog + code-of-conduct diff --git a/documentation/source/install.rst b/documentation/source/install.rst new file mode 100644 index 0000000..2cff867 --- /dev/null +++ b/documentation/source/install.rst @@ -0,0 +1,11 @@ +******* +Install +******* + +.. code-block:: bash + + $ pip install flat_tree + +.. note:: + + Only Python >= 3.6 is supported. diff --git a/documentation/source/modules-api.rst b/documentation/source/modules-api.rst new file mode 100644 index 0000000..d2d6158 --- /dev/null +++ b/documentation/source/modules-api.rst @@ -0,0 +1,9 @@ +*********** +Modules API +*********** + +.. automodule:: flat_tree.accessor + :members: + +.. automodule:: flat_tree.iterator + :members: diff --git a/documentation/source/other-impls.rst b/documentation/source/other-impls.rst new file mode 100644 index 0000000..ab4a404 --- /dev/null +++ b/documentation/source/other-impls.rst @@ -0,0 +1,9 @@ +.. _other-implementations: + +Other Implementations +********************* + +* https://github.com/mafintosh/flat-tree +* https://github.com/datrs/flat-tree +* https://github.com/bcomnes/flattree +* https://github.com/datcxx/flat-tree diff --git a/flat_tree/__init__.py b/flat_tree/__init__.py new file mode 100644 index 0000000..032afb0 --- /dev/null +++ b/flat_tree/__init__.py @@ -0,0 +1,15 @@ +"""Flat tree module.""" + +from flat_tree.accessor import FlatTreeAccessor # noqa +from flat_tree.iterator import FlatTreeIterator # noqa + +try: + import pkg_resources +except ImportError: + pass + + +try: + __version__ = pkg_resources.get_distribution('flat_tree').version +except Exception: + __version__ = 'unknown' diff --git a/flat_tree/accessor.py b/flat_tree/accessor.py new file mode 100644 index 0000000..76c9827 --- /dev/null +++ b/flat_tree/accessor.py @@ -0,0 +1,205 @@ +"""An accessor for navigating flat trees.""" + +__all__ = ['FlatTreeAccessor'] + +from typing import List, Optional + + +class FlatTreeAccessor: + """A flat tree accessor.""" + + def index(self, depth: int, offset: int) -> int: + """The tree index specified by the depth and offset. + + :param depth: The depth of the tree + :param offset: The offset from left hand side of the tree + """ + return ((1 + (2 * offset)) * (2 ** depth)) - 1 + + def offset(self, index: int, depth: Optional[int] = None) -> int: + """The offset of given index from the left hand side of the tree. + + :param index: The tree index + :param depth: The depth of the tree + """ + if not (index & 1): + return int(index / 2) + + if depth is None: + depth = self.depth(index) + + return int((((index + 1) / (2 ** depth)) - 1) / 2) + + def depth(self, index: int) -> int: + """The depth of the given index in the tree. + + :param index: The tree index + """ + depth = 0 + + index += 1 + while not (index & 1): + depth += 1 + index = index >> 1 + + return depth + + def parent(self, index: int, depth: Optional[int] = None) -> int: + """The index of the parent relative to the given index. + + :param index: The index relative to the parent + :param depth: The depth of the index + """ + if depth is None: + depth = self.depth(index) + + offset = self.offset(index, depth) + + return self.index(depth + 1, int((offset - (offset & 1)) / 2)) + + def sibling(self, index: int, depth: Optional[int] = None) -> int: + """The index of the sibling relative to the given index. + + :param index: The index relative to the sibling + :param depth: The depth of the index + """ + if depth is None: + depth = self.depth(index) + + offset = self.offset(index, depth) + offset = offset - 1 if (offset & 1) else offset + 1 + + return self.index(depth, offset) + + def children(self, index: int, depth: Optional[int] = None) -> List[int]: + """All children relative to the given index. + + :param index: The parent index + :param depth: The depth of the index + """ + if not (index & 1): + return [] + + if not depth: + depth = self.depth(index) + + offset = self.offset(index, depth) * 2 + + return [ + self.index((depth - 1), offset), + self.index((depth - 1), (offset + 1)), + ] + + def spans(self, index: int, depth: Optional[int] = None) -> List[int]: + """The span of the tree. + + :param index: The index of the root + :param depth: The depth of the index + """ + if not (index & 1): + return [index, index] + + if not depth: + depth = self.depth(index) + + offset = self.offset(index, depth) + width = 2 ** (depth + 1) + + return [(offset * width), ((offset + 1) * width) - 2] + + def left_span(self, index: int, depth: Optional[int] = None) -> int: + """The leftmost span of the tree. + + :param index: The index of the tree root + :param depth: The depth of the index + """ + if not (index & 1): + return index + + if not depth: + depth = self.depth(index) + + return self.offset(index, depth) * (2 ** (depth + 1)) + + def right_span(self, index: int, depth: Optional[int] = None) -> int: + """The rightmost span of the tree. + + :param index: The index of the tree root + :param depth: The depth of the index + """ + if not (index & 1): + return index + + if not depth: + depth = self.depth(index) + + return (self.offset(index, depth) + 1) * (2 ** (depth + 1)) - 2 + + def count(self, index: int, depth: Optional[int] = None) -> int: + """The number of nodes a tree contains. + + :param index: The index of the root of the tree + :param depth: The depth of the root of the tree + """ + if not (index & 1): + return 1 + + if not depth: + depth = self.depth(index) + + return (2 ** (depth + 1)) - 1 + + def full_roots(self, index: int) -> List[int]: + """All full roots within the tree. + + :param index: The index of the root of the tree + """ + if index & 1: + message = 'Roots only available for tree depth 0' + raise ValueError(message) + + roots: List[int] = [] + + index = int(index / 2) + offset, factor = 0, 1 + + while True: + if not index: + return roots + + while (factor * 2) <= index: + factor = factor * 2 + + roots.append((offset + factor) - 1) + + offset = (offset + 2) * factor + index = index - factor + factor = 1 + + def left_child(self, index: int, depth: Optional[int] = None) -> int: + """The left child of the given index. + + :param index: The index of the tree + :param depth: The depth of the tree + """ + if not (index & 1): + return -1 + + if not depth: + depth = self.depth(index) + + return self.index((depth - 1), (self.offset(index, depth) * 2)) + + def right_child(self, index: int, depth: Optional[int] = None) -> int: + """The right child of the given index. + + :param index: The index of the tree + :param depth: The depth of the tree + """ + if not (index & 1): + return -1 + + if not depth: + depth = self.depth(index) + + return self.index((depth - 1), (1 + (self.offset(index, depth) * 2))) diff --git a/flat_tree/iterator.py b/flat_tree/iterator.py new file mode 100644 index 0000000..e0abf86 --- /dev/null +++ b/flat_tree/iterator.py @@ -0,0 +1,111 @@ +"""Stateful iterator for flat trees.""" + +__all__ = ['FlatTreeIterator'] + +import attr + +from flat_tree.accessor import FlatTreeAccessor + + +@attr.s(auto_attribs=True) +class FlatTreeIterator: + """Stateful iterator for flat trees.""" + + index: int = 0 + offset: int = 0 + factor: int = 0 + accessor: FlatTreeAccessor = FlatTreeAccessor() + + def __attrs_post_init__(self): + self.seek(self.index) + + def next(self) -> int: + """The next index in the tree.""" + self.offset += 1 + self.index += self.factor + return self.index + + def prev(self) -> int: + """The previous index in the tree.""" + if not self.offset: + return self.index + + self.offset -= 1 + self.index = self.factor + + return self.index + + def seek(self, index: int) -> None: + """Move iterator to the given index. + + :param index: The index to move to + """ + self.index = index + + if self.index & 1: + self.offset = self.accessor.offset(index) + self.factor = 2 ** (self.accessor.depth(index) + 1) + else: + self.offset = int(index / 2) + self.factor = 2 + + def parent(self) -> int: + """Move iterator to the parent index.""" + if self.offset & 1: + self.index -= int(self.factor / 2) + self.offset = int((self.offset - 1) / 2) + else: + self.index += int(self.factor / 2) + self.offset = int(self.offset / 2) + + self.factor *= 2 + + return self.index + + def left_child(self) -> int: + """Move iterator to the left child.""" + if self.factor == 2: + return self.index + + self.factor = int(self.factor / 2) + self.index -= int(self.factor / 2) + self.offset *= 2 + + return self.index + + def right_child(self) -> int: + """Move iterator to the right child.""" + if self.factor == 2: + return self.index + + self.factor = int(self.factor / 2) + self.index += int(self.factor / 2) + self.offset = (2 * self.offset) + 1 + + return self.index + + def left_span(self) -> int: + """Move iterator to the left span.""" + self.index = int(self.index - (self.factor / 2)) + 1 + self.offset = int(self.index / 2) + self.factor = 2 + return self.index + + def right_span(self) -> int: + """Move iterator to the right span.""" + self.index = int((self.index - self.factor) / 2) - 1 + self.offset = int(self.index / 2) + self.factor = 2 + return self.index + + def sibling(self) -> int: + """Move iterator to the sibling.""" + return self.next() if self.is_left() else self.prev() + + def is_left(self) -> bool: + """Is this index a left sibling?""" + return not self.offset & 1 + + def is_right(self) -> bool: + """Is this index a right sibling?""" + return not self.is_left() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..3580d9a --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +python_version = 3.7 +platform = linux +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1825edf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = [ + "setuptools>=40.9.0", + "setuptools-scm", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 80 +target-version = ['py35', 'py36', 'py37'] +skip-string-normalization = true +include = '\.pyi?$' + +[tool.towncrier] +directory = "changelog/" +filename = "CHANGELOG.rst" +package = "flat_tree" +package_dir = "flat_tree" + + [[tool.towncrier.type]] + directory = "removal" + name = "Removals" + showcontent = true + + [[tool.towncrier.type]] + directory = "deprecation" + name = "Deprecations" + showcontent = true + + [[tool.towncrier.type]] + directory = "feature" + name = "Features" + showcontent = true + + [[tool.towncrier.type]] + directory = "bugfix" + name = "Bug Fixes" + showcontent = true + + [[tool.towncrier.type]] + directory = "doc" + name = "Improved Documentation" + showcontent = true + + [[tool.towncrier.type]] + directory = "trivial" + name = "Trivial/Internal Changes" + showcontent = true + + [[tool.towncrier.type]] + directory = "announce" + name = "Project Announcements" + showcontent = true diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b49cf12 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,64 @@ +[tool:pytest] +testpaths = test + +[flake8] +max-line-length = 80 + +[isort] +known_first_party = flat_tree +known_third_party = pytest +line_length = 80 +multi_line_output = 3 +skip = .venv, .tox +include_trailing_comma = True + +[metadata] +name = flat-tree +author = decentral1se +author_email = lukewm@riseup.net +maintainer = decentral1se +maintainer_email = lukewm@riseup.net +url = https://hack.decentral1.se/datpy/flat-tree.git +project_urls = + Source Code = https://hack.decentral1.se/datpy/flat-tree.git + Changelog = https://flat-tree.readthedocs.io/en/latest/changelog.html + Documentation = https://flat-tree.readthedocs.io/ + Maintainer Support = https://decentral1.se/ +description = Utilities for navigating flat trees +long_description = file: README.rst +license = MIT +license_file = LICENSE +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +use_scm_version = True +python_requires = !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +setup_requires = + setuptools_scm + setuptools_scm_git_archive +package_dir = + = . +packages = find: +zip_safe = False +install_requires = + attrs >= 19.1.0, < 20.0 + +[options.packages.find] +where = . + +[build_sphinx] +all_files = 1 +build-dir = documentation/build +source-dir = documentation/source +warning-is-error = True + +[options.extras_require] +docs = + sphinx + sphinx-autodoc-typehints >= 1.6.0, < 2.0 +changelog = + towncrier <= 19.2.0, < 20.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d5d43d7 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup(use_scm_version=True) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..79a1163 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from flat_tree.accessor import FlatTreeAccessor + + +@pytest.fixture +def tree(): + return FlatTreeAccessor() + + +@pytest.fixture +def FlatTreeIterator(): + from flat_tree.iterator import FlatTreeIterator + + return FlatTreeIterator diff --git a/test/test_accessor.py b/test/test_accessor.py new file mode 100644 index 0000000..a7a2fb6 --- /dev/null +++ b/test/test_accessor.py @@ -0,0 +1,120 @@ +import pytest + + +def test_index(tree): + assert tree.index(0, 0) == 0 + assert tree.index(0, 1) == 2 + assert tree.index(0, 2) == 4 + + +def test_depth(tree): + assert tree.depth(5) == 1 + assert tree.depth(3) == 2 + assert tree.depth(4) == 0 + + +def test_offset(tree): + assert tree.offset(0) == 0 + assert tree.offset(1) == 0 + assert tree.offset(2) == 1 + assert tree.offset(3) == 0 + assert tree.offset(4) == 2 + + assert isinstance(tree.offset(0), int) + + +def test_parent(tree): + assert tree.index(1, 0) == 1 + assert tree.index(1, 1) == 5 + assert tree.index(2, 0) == 3 + + assert tree.parent(0) == 1 + assert tree.parent(2) == 1 + assert tree.parent(1) == 3 + + +def test_sibling(tree): + assert tree.sibling(0) == 2 + assert tree.sibling(2) == 0 + assert tree.sibling(1) == 5 + assert tree.sibling(5) == 1 + + +def test_children(tree): + assert tree.children(0) == [] + assert tree.children(1) == [0, 2] + assert tree.children(3) == [1, 5] + assert tree.children(9) == [8, 10] + + +def test_count(tree): + assert tree.count(0) == 1 + assert tree.count(1) == 3 + assert tree.count(3) == 7 + assert tree.count(5) == 3 + assert tree.count(23) == 15 + assert tree.count(27) == 7 + + +def test_spans(tree): + assert tree.spans(0) == [0, 0] + assert tree.spans(1) == [0, 2] + assert tree.spans(3) == [0, 6] + assert tree.spans(23) == [16, 30] + assert tree.spans(27) == [24, 30] + + +def test_left_span(tree): + assert tree.left_span(0) == 0 + assert tree.left_span(1) == 0 + assert tree.left_span(3) == 0 + assert tree.left_span(23) == 16 + assert tree.left_span(27) == 24 + + +def test_right_span(tree): + assert tree.right_span(0) == 0 + assert tree.right_span(1) == 2 + assert tree.right_span(3) == 6 + assert tree.right_span(23) == 30 + assert tree.right_span(27) == 30 + + +def test_full_roots(tree): + assert tree.full_roots(0) == [] + assert tree.full_roots(2) == [0] + assert tree.full_roots(8) == [3] + assert tree.full_roots(20) == [7, 17] + assert tree.full_roots(18) == [7, 16] + assert tree.full_roots(16) == [7] + + with pytest.raises(ValueError): + tree.full_roots(1) + + +def test_left_child(tree): + assert tree.left_child(0) == -1 + assert tree.left_child(1) == 0 + assert tree.left_child(3) == 1 + + +def test_right_child(tree): + assert tree.right_child(0) == -1 + assert tree.right_child(1) == 2 + assert tree.right_child(3) == 5 + + +def test_parent_big_index(tree): + assert tree.parent(10000000000) == 10000000001 + + +def test_child_parent_child(tree): + child = 0 + + for _ in range(50): + child = tree.parent(child) + assert child == 1125899906842623 + + for _ in range(50): + child = tree.left_child(child) + assert child == 0 diff --git a/test/test_iterator.py b/test/test_iterator.py new file mode 100644 index 0000000..80b31bd --- /dev/null +++ b/test/test_iterator.py @@ -0,0 +1,23 @@ +def test_iter_from_leaf(FlatTreeIterator): + tree_iter = FlatTreeIterator() + + assert tree_iter.index == 0 + assert tree_iter.parent() == 1 + assert tree_iter.parent() == 3 + assert tree_iter.parent() == 7 + assert tree_iter.right_child() == 11 + assert tree_iter.left_child() == 9 + assert tree_iter.next() == 13 + assert tree_iter.left_span() == 12 + + +def test_iter_not_from_leaf(FlatTreeIterator): + tree_iter = FlatTreeIterator(index=1) + + assert tree_iter.index == 1 + assert tree_iter.parent() == 3 + assert tree_iter.parent() == 7 + assert tree_iter.right_child() == 11 + assert tree_iter.left_child() == 9 + assert tree_iter.next() == 13 + assert tree_iter.left_span() == 12 diff --git a/test/test_version.py b/test/test_version.py new file mode 100644 index 0000000..b5ab8b8 --- /dev/null +++ b/test/test_version.py @@ -0,0 +1,9 @@ +"""Version test module.""" + + +def test_version_fails_gracefully(mocker): + target = 'pkg_resources.get_distribution' + with mocker.patch(target, side_effect=Exception()): + from flat_tree.__init__ import __version__ + + assert __version__ == 'unknown' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b382f80 --- /dev/null +++ b/tox.ini @@ -0,0 +1,86 @@ +[tox] +envlist = + {py36,py37}-test + lint + sort + format + type + docs + changelog + metadata-release +skip_missing_interpreters = True +isolated_build = True + +[testenv] +description = run the unit tests +deps = + pytest + pytest-cov + pytest-mock +commands = + pytest test/ --cov={toxinidir}/flat_tree/ --no-cov-on-fail {posargs} + +[testenv:lint] +description = lint the source +skipdist = True +deps = + flake8 +commands = + flake8 {posargs} flat_tree/ test/ + +[testenv:sort] +description = sort the source +skipdist = True +deps = + isort +commands = + isort {posargs:-rc -c} -sp setup.cfg flat_tree/ test/ + +[testenv:format] +description = format the source +skipdist = True +basepython = python3.6 +deps = + black +commands = + black {posargs:--check} flat_tree/ test/ + +[testenv:type] +description = type check the source +basepython = python3.7 +skipdist = True +deps = + mypy +commands = + mypy flat_tree/ test/ + +[testenv:docs] +description = build the documentation +deps = + sphinx + sphinx-autodoc-typehints >= 1.6.0, < 2.0 +commands = + python -m setup build_sphinx + +[testenv:changelog] +description = draft the changelog +skipdist = True +deps = + towncrier +commands = + towncrier --draft + +[testenv:metadata-release] +description = validate the package metadata +deps = + twine +commands = + twine check .tox/dist/* + +[testenv:release] +description = make a release +deps = + {[testenv:metadata-release]deps} +commands = + python -m setup sdist bdist_wheel + twine upload {toxworkdir}/dist/*