flat-tree implementation coming down the tubes

This commit is contained in:
Luke Murphy 2019-06-20 17:12:38 +02:00
commit 8626785ca3
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
32 changed files with 1078 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
*.egg-info/
*.pyc
.coverage
.eggs/
.mypy_cache/
.tox/
.venv/
__pycache__
build/
dist/
pip-wheel-metadata/
documentation/build/

16
.readthedocs.yml Normal file
View File

@ -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

7
CHANGELOG.rst Normal file
View File

@ -0,0 +1,7 @@
Flat_Tree 0.0.1a2 (2019-07-01)
==============================
Project Announcements
---------------------
- Initial development release is made! (#1)

111
CODE_OF_CONDUCT.rst Normal file
View File

@ -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

67
CONTRIBUTING.rst Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include LICENSE README.rst CHANGELOG.rst

55
README.rst Normal file
View File

@ -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

12
documentation/Makefile Normal file
View File

@ -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)

View File

@ -0,0 +1,5 @@
*********
Changelog
*********
.. include:: ../../CHANGELOG.rst

View File

@ -0,0 +1,6 @@
.. _code-of-conduct:
Code of Conduct
***************
.. include:: ../../CODE_OF_CONDUCT.rst

View File

@ -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']

View File

@ -0,0 +1,5 @@
**********
Contribute
**********
.. include:: ../../CONTRIBUTING.rst

View File

@ -0,0 +1,14 @@
.. include:: ../../README.rst
:end-before: _documentation
Table of Contents
*****************
.. toctree::
install
modules-api
other-impls
contribute
changelog
code-of-conduct

View File

@ -0,0 +1,11 @@
*******
Install
*******
.. code-block:: bash
$ pip install flat_tree
.. note::
Only Python >= 3.6 is supported.

View File

@ -0,0 +1,9 @@
***********
Modules API
***********
.. automodule:: flat_tree.accessor
:members:
.. automodule:: flat_tree.iterator
:members:

View File

@ -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

15
flat_tree/__init__.py Normal file
View File

@ -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'

205
flat_tree/accessor.py Normal file
View File

@ -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)))

111
flat_tree/iterator.py Normal file
View File

@ -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()

4
mypy.ini Normal file
View File

@ -0,0 +1,4 @@
[mypy]
python_version = 3.7
platform = linux
ignore_missing_imports = True

54
pyproject.toml Normal file
View File

@ -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

64
setup.cfg Normal file
View File

@ -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

3
setup.py Normal file
View File

@ -0,0 +1,3 @@
from setuptools import setup
setup(use_scm_version=True)

0
test/__init__.py Normal file
View File

15
test/conftest.py Normal file
View File

@ -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

120
test/test_accessor.py Normal file
View File

@ -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

23
test/test_iterator.py Normal file
View File

@ -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

9
test/test_version.py Normal file
View File

@ -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'

86
tox.ini Normal file
View File

@ -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/*