From 54a91655ad66c869181f16031b71601c16d70c71 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Sun, 21 Dec 2025 06:16:14 -0700 Subject: [PATCH 01/10] ci: test local pip install --- .github/workflows/tests.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5e0364d..0da8cf8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,3 +113,18 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + pip-install-local: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: 3.14 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + - name: Test pip install + run: | + pip install . + python -c 'from intervaltree import IntervalTree; t = IntervalTree(); print(t)' From cdda73db15d6b4f11cb4e5ce61e3c2c698b6eb0f Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Sun, 21 Dec 2025 06:53:02 -0700 Subject: [PATCH 02/10] build: hoist hatchling config above project, as in docs --- pyproject.toml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17312cb..a4cc8c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,11 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "intervaltree" description='Editable interval tree data structure for Python 2 and 3' -version = "3.2.0" +version = "3.2.1" authors = [ { name="Chaim Leib Halbert", email="chaim.leib.halbert@gmail.com" }, { name="Konstantin Tretyakov" }, @@ -68,9 +72,3 @@ norecursedirs = [ '.tox', '.vscode', ] - -[tool.hatch] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" From 5e9ed9e81ca72d71049c7cb28f616eb7938e2804 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Sun, 21 Dec 2025 14:40:10 -0700 Subject: [PATCH 03/10] chore: rm .ackrc --- .ackrc | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .ackrc diff --git a/.ackrc b/.ackrc deleted file mode 100644 index 7c2ca64..0000000 --- a/.ackrc +++ /dev/null @@ -1,2 +0,0 @@ ---ignore-dir=intervaltree.egg-info ---norst From ac9b3b25f3a9e09c00c93e5126ca67a778d9278a Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Sun, 21 Dec 2025 18:29:10 -0700 Subject: [PATCH 04/10] build: update Makefile --- .gitignore | 1 + Makefile | 205 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 135 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 3cf0755..49192e5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pyandoc/ pandoc docutils/ bin/ +venv/ # Developer eggs *.egg-info diff --git a/Makefile b/Makefile index 868c1e0..7791acf 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,5 @@ SHELL=bash -SCRIPTS_DIR:=$(PWD)/scripts - # any files ending in .py?, and any folders named __pycache__ TEMPS=$(shell \ find intervaltree test \ @@ -9,49 +7,70 @@ TEMPS=$(shell \ -o \( -type d -name '__pycache__' \) \ ) -PYTHONS:=2.7.18 3.6.15 3.7.16 3.8.16 3.9.16 3.10.10 3.11.2 -PYTHON_MAJORS:=$(shell \ - echo "$(PYTHONS)" | \ - tr ' ' '\n' | cut -d. -f1 | \ - uniq \ -) -PYTHON_MINORS:=$(shell \ - echo "$(PYTHONS)" | \ - tr ' ' '\n' | cut -d. -f1,2 | \ - uniq \ -) +PYTHON_MINORS:=$(shell echo 2.7 3.{5..14}) +PYTHONS:=$(shell echo $(PYTHON_MINORS) | xargs -n1 pyenv latest -k) +MAINPY=$(shell echo $(PYTHONS) | tr ' ' '\n' | tail -n1) +MAINPYMINOR=$(shell echo $(PYTHON_MINORS) | tr ' ' '\n' | tail -n1) # PyPI server name, as specified in ~/.pypirc # See http://peterdowns.com/posts/first-time-with-pypi.html PYPI=pypitest -TWINE=twine # default target all: test -test: pytest +test: pytest flake8 quicktest: - PYPI=$(PYPI) python setup.py test + source venv/mainpy$(MAINPY)/bin/activate + python -m pytest coverage: - coverage run --source=intervaltree setup.py develop test - coverage report - coverage html + pytest --cov=intervaltree + coverage html | sed -E 's@(Wrote HTML report to) (.*)@\1 file://'"$$PWD"'/\2@' -pytest: deps-dev - PYTHONS="$(PYTHONS)" PYTHON_MINORS="$(PYTHON_MINORS)" "$(SCRIPTS_DIR)/testall.sh" +pytest: + unset anyerr; \ + for pyver in $(PYTHONS); do \ + echo "pytest in Python $${pyver}"; \ + pyminor=$${pyver%.*}; \ + if ! source venv/py$${pyminor}/bin/activate; then \ + echo "Failed to activate" >&2; \ + anyerr=y; \ + continue; \ + fi; \ + >/dev/null echo "Python 2.7 sends version to stderr instead of stdout"; \ + if [[ "$$(python$${pyminor} --version 2>&1)" != *"Python $${pyver}" ]]; then \ + echo "venv does not have Python $${pyver} installed" >&2; \ + anyerr=y; \ + continue; \ + fi; \ + if ! python$${pyminor} -m pytest; then \ + echo "Pytest failed" >&2; \ + anyerr=y; \ + continue; \ + fi; \ + done; \ + if [ -n "$$anyerr" ]; then exit 1; fi + +flake8: + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --extend-exclude=venv,requirements,dist + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics clean: clean-build clean-eggs clean-temps -distclean: clean +distclean: clean clean-env clean-build: - rm -rf dist build + rm -rf dist build htmlcov clean-eggs: rm -rf *.egg* .eggs/ +clean-env: + rm -rf venv + clean-temps: rm -rf $(TEMPS) @@ -66,90 +85,134 @@ install-pypi: pip install intervaltree install-develop: - PYPI=$(PYPI) python setup.py develop + pip install -e . uninstall: - pip uninstall intervaltree - -# Register at PyPI -register: - PYPI=$(PYPI) python setup.py register -r $(PYPI) + pip uninstall intervaltree sortedcontainers # Setup for live upload release: $(eval PYPI=pypi) -# Build source distribution -sdist-build: distclean deps-dev - PYPI=$(PYPI) python setup.py sdist - # Build dist distribution -bdist-build: distclean deps-dev - PYPI=$(PYPI) python setup.py bdist_wheel +build: clean + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + python$(MAINPYMINOR) -m hatch build -dist-upload: sdist-build bdist-build +upload: test build + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 if [[ "$(PYPI)" == pypitest ]]; then \ - $(TWINE) upload --repository-url https://test.pypi.org/legacy/ dist/*; \ + python$(MAINPYMINOR) -m twine upload --repository-url testpypi dist/*; \ else \ - $(TWINE) upload dist/*; \ + python$(MAINPYMINOR) -m twine upload dist/*; \ fi -deps-dev: pyenv-install-versions +install-devtools: \ + pyenv-install-main-env \ + install-flake8 \ + install-hatchling \ + install-twine \ + pyenv-install-envs + +install-flake8: + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + pip$(MAINPYMINOR) install -r requirements/flake8.txt + +install-twine: + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + pip$(MAINPYMINOR) install -U twine -# Uploads to test server, unless the release target was run too -upload: test clean dist-upload +install-hatchling: + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + pip$(MAINPYMINOR) install -U hatchling pyenv-is-installed: pyenv --version &>/dev/null || (echo "ERROR: pyenv not installed" && false) -pyenv-install-versions: pyenv-is-installed - for pyver in $(PYTHONS); do (echo N | pyenv install $$pyver) || true; done +pyenv-install-main-env: pyenv-is-installed + @# echo N: Do not recompile a python interpreter if it is already present. + echo N | pyenv install $(MAINPY) || true + @echo "Setting up venv for main Python version $(MAINPY)" + export PYENV_VERSION=$(MAINPY) + unset CFLAGS; + python$(MAINPYMINOR) -m venv venv/mainpy$(MAINPYMINOR) || exit 1 + source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + pip$(MAINPYMINOR) install -U pip || exit 1 + pip$(MAINPYMINOR) install -r requirements/pytest.txt || exit 1 + @echo "Finished setting up venv for main Python $(MAINPY)" + +pyenv-install-envs: pyenv-is-installed + @# echo N: Do not recompile a python interpreter if it is already present. + for pyver in $(PYTHONS); do echo N | pyenv install $$pyver || true; done + @# Setup virtual environments. + @# venv only works on Python 3.3 and later. It will not work for 2.7. + @# So, use virtualenv for 2.7. for pyver in $(PYTHONS); do \ + echo ""; \ + echo "Setting up venv for Python $${pyver}"; \ export PYENV_VERSION=$$pyver; \ - pip install -U pip; \ - pip install -U pytest; \ - done | grep -v 'Requirement already satisfied, skipping upgrade' - # twine and wheel needed only under latest PYTHONS version for uploading to PYPI - export PYENV_VERSION=$(shell \ - echo $(PYTHONS) | \ - tr ' ' '\n' | \ - tail -n1 \ - ) - pip install -U twine - pip install -U wheel - pyenv rehash + pyminor=$${pyver%.*}; \ + unset CFLAGS; \ + if [[ "$$pyver" == 2.* ]]; then \ + export CFLAGS=" -std=c17"; \ + pip$${pyminor} install virtualenv || continue; \ + python$${pyminor} -m virtualenv \ + --python=python$${pyminor} \ + venv/py$${pyminor} || continue; \ + source venv/py$${pyminor}/bin/activate || continue; \ + else \ + python$${pyminor} -m venv venv/py$${pyminor} || continue; \ + source venv/py$${pyminor}/bin/activate || continue; \ + fi; \ + pip$${pyminor} install -U pip || continue; \ + pip$${pyminor} install -r requirements/pytest.txt || continue; \ + echo "Finished setting up venv for Python $${pyver}"; \ + done + +pyenv-uninstall-envs: + rm -rf venv + +pyenv-uninstall-versions: + for pyver in $(PYTHONS); do (echo y | pyenv uninstall $$pyver) || true; done # for debugging the Makefile env: @echo @echo TEMPS="\"$(TEMPS)\"" @echo PYTHONS="\"$(PYTHONS)\"" - @echo PYTHON_MAJORS="\"$(PYTHON_MAJORS)\"" @echo PYTHON_MINORS="\"$(PYTHON_MINORS)\"" + @echo MAINPY="\"$(MAINPY)\"" + @echo MAINPYMINOR="\"$(MAINPYMINOR)\"" + @echo PYENV_VERSION="\"$(PYENV_VERSION)\"" @echo PYPI="\"$(PYPI)\"" .PHONY: all \ - test \ - quicktest \ - pytest \ + build \ clean \ - distclean \ clean-build \ clean-eggs \ clean-temps \ - install-testpypi \ - install-pypi \ + coverage \ + distclean \ + env \ + flake8 \ install-develop \ - pyenv-install-versions \ + install-devtools \ + install-flake8 \ + install-hatchling \ + install-pypi \ + install-testpypi \ + install-twine \ + pyenv-install-envs \ + pyenv-install-main-env \ pyenv-is-installed \ - uninstall \ - register \ + pyenv-uninstall-envs \ + pyenv-uninstall-versions \ + pytest \ + quicktest \ release \ - sdist-upload \ - deps-ci \ - deps-dev \ - pm-update \ - upload \ - env + test \ + uninstall \ + upload From 8dad78aab425fd30e47116659329e8bb0686728a Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 02:24:49 -0700 Subject: [PATCH 05/10] build: merge installer targets, verbose twine, fix --repository-url to -r --- Makefile | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 7791acf..61e37cf 100644 --- a/Makefile +++ b/Makefile @@ -102,29 +102,12 @@ build: clean upload: test build source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 if [[ "$(PYPI)" == pypitest ]]; then \ - python$(MAINPYMINOR) -m twine upload --repository-url testpypi dist/*; \ + python$(MAINPYMINOR) -m twine upload --verbose -r testpypi dist/*; \ else \ - python$(MAINPYMINOR) -m twine upload dist/*; \ + python$(MAINPYMINOR) -m twine upload --verbose dist/*; \ fi -install-devtools: \ - pyenv-install-main-env \ - install-flake8 \ - install-hatchling \ - install-twine \ - pyenv-install-envs - -install-flake8: - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - pip$(MAINPYMINOR) install -r requirements/flake8.txt - -install-twine: - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - pip$(MAINPYMINOR) install -U twine - -install-hatchling: - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - pip$(MAINPYMINOR) install -U hatchling +install-devtools: pyenv-install-main-env pyenv-install-envs pyenv-is-installed: pyenv --version &>/dev/null || (echo "ERROR: pyenv not installed" && false) @@ -138,7 +121,9 @@ pyenv-install-main-env: pyenv-is-installed python$(MAINPYMINOR) -m venv venv/mainpy$(MAINPYMINOR) || exit 1 source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 pip$(MAINPYMINOR) install -U pip || exit 1 + pip$(MAINPYMINOR) install -r requirements/flake8.txt pip$(MAINPYMINOR) install -r requirements/pytest.txt || exit 1 + pip$(MAINPYMINOR) install -U hatchling twine @echo "Finished setting up venv for main Python $(MAINPY)" pyenv-install-envs: pyenv-is-installed @@ -199,11 +184,8 @@ env: flake8 \ install-develop \ install-devtools \ - install-flake8 \ - install-hatchling \ install-pypi \ install-testpypi \ - install-twine \ pyenv-install-envs \ pyenv-install-main-env \ pyenv-is-installed \ From 2a0d080457419e79f3de4d364cf84dc555f52085 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 02:25:25 -0700 Subject: [PATCH 06/10] build: uv-dynamic-versioning --- Makefile | 2 +- pyproject.toml | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 61e37cf..8c3d1ed 100644 --- a/Makefile +++ b/Makefile @@ -123,7 +123,7 @@ pyenv-install-main-env: pyenv-is-installed pip$(MAINPYMINOR) install -U pip || exit 1 pip$(MAINPYMINOR) install -r requirements/flake8.txt pip$(MAINPYMINOR) install -r requirements/pytest.txt || exit 1 - pip$(MAINPYMINOR) install -U hatchling twine + pip$(MAINPYMINOR) install -U hatch hatchling twine @echo "Finished setting up venv for main Python $(MAINPY)" pyenv-install-envs: pyenv-is-installed diff --git a/pyproject.toml b/pyproject.toml index a4cc8c5..bbd8af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,18 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" +[tool.uv-dynamic-versioning] +dirty = true +pattern = "default-unprefixed" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + [project] name = "intervaltree" -description='Editable interval tree data structure for Python 2 and 3' -version = "3.2.1" +description = 'Editable interval tree data structure for Python 2 and 3' +dynamic = ['version'] authors = [ { name="Chaim Leib Halbert", email="chaim.leib.halbert@gmail.com" }, { name="Konstantin Tretyakov" }, From e74b4f9f884d44b542b94f40723923f1e713e7a8 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 19:36:08 -0700 Subject: [PATCH 07/10] build: fix Makefile env scopes, make all includes build target, upd HACKING.md --- HACKING.md | 202 ++++++++++++++++++++++++++++++----------------------- Makefile | 110 ++++++++++++++++++----------- 2 files changed, 183 insertions(+), 129 deletions(-) diff --git a/HACKING.md b/HACKING.md index 6c73a30..18ff617 100644 --- a/HACKING.md +++ b/HACKING.md @@ -5,152 +5,180 @@ This is a developer's guide to modifying and maintaining `intervaltree`. ## Dependencies -* On a Mac, you will need [`brew`][brew]. +Before running most `make` commands, +you will need to [install pyenv] and run `make install-devtools`. +This section describes those steps in detail. -* On Linux, you will need `apt-get`. +### Pyenv -On all systems, Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5 are needed to run the complete test suite. +To install pyenv, follow your preferred instructions at +https://github.com/pyenv/pyenv/blob/master/README.md#installation +including -### Single version of Python - -If you don't have all the above versions of Python, these `make` features will be unavailable to you: +1. running the installer, +2. the modifications to your shell's rc file, and +3. restarting your shell. -* `make` and `make test` -* `make pytest` -* `make upload` and `make release upload` - +### Set up the virtual environments (venv directory) -## Project structure +This section creates the virtual environments necessary for testing `intervaltree` +in all the supported versions of Python, locally. -### `intervaltree` +#### Optional: Compiling Python with third-party libraries -The `intervaltree` directory has three main files: +If certain libraries and headers are present, +pyenv will compile the Python interpreters with the associated support. +Note that as we go back in Python version history, +support for the latest libraries declines. +Incompatible libraries will be ignored, rather than break compilation. -* `intervaltree.py` -* `interval.py` -* `node.py` +If readline is provided, this provides autocomplete and advanced history support +when the interpreters are run interactively. +On systems using `dnf`, readline-devel also includes ncurses-devel. -`intervaltree.py` and `interval.py` contain the public API to `IntervalTree` and `Interval`. `node.py` contains the internal logic of the tree. For the theory of how this type of tree works, read the following: +```bash +apt-get install libreadline-dev libncurses5-dev +# OR +dnf install readline-devel +``` -* Wikipedia's [Interval tree][Wiki intervaltree] article -* Eternally Confuzzled's tutorial on [AVL balancing][Confuzzled AVL tree] -* Tyler Kahn's simpler, immutable [interval tree implementation][Kahn intervaltree] in Python - -### `test` +More information about suggested packages is listed at +https://github.com/pyenv/pyenv/wiki#suggested-build-environment -All files ending with `_test.py` are detected and run whenever you run `make` or `make quicktest`. In those files, only functions beginning with `test_` are executed. +#### Install Python versions and dev tools -#### `test/data` -Some tests depend on having certain lists of `Interval`s. These are stored in the modules of `test/data`. Most of these modules only contain a `data` attribute, which is a list of tuples that is converted to a list of `Interval`s by `test/intervals.py`. You can access them by importing the dict of lists of `Interval`s `test.intervals.ivs`. +To install the Python versions and dev tools: -Other tests (like `test/issue25_test.py`) depend on having pre-constructed `IntervalTree`s. These are constructed by `test/intervaltrees.py` and can be accessed by importing `test.intervaltrees.trees`. This is a dict of callables that return `IntervalTree`s. +```bash +make install-devtools +``` -### `scripts` +This does the following: -Contains `testall.sh`, which runs all tests on all supported versions of Python. +1. Uses pyenv to download (and if no binary is hosted, compile) +all the Python interpreters which we test under. +2. Creates Python virtual environments under the venv directory. +3. Installs the needed packages in each venv for testing and deployment. -### Dependency folders - -* `pyandoc` and `pandoc` give the ability to convert README.md into rst format for PyPI. -* `docutils` and `bin` are created if `pip` could not install them system-wide without permissions. These are used to check the syntax of the converted rst README. -* `*.egg` are created by `PyTest` to fulfill testing dependencies -* `intervaltree.egg-info` is package metadata generated by `setup.py`. - -### Other documentation files - -* `HACKING.md` is this file. -* `README.md` contains public API documentation and credits. -* `README.rst` is generated from `README.md`. -* `CHANGELOG.md` -* `LICENSE.txt` +## Testing -### Other code files +To run the tests in the `test` directory, run -* `Makefile` contains convenience routines for managing and testing the project. It installs certain dependencies that are too inconvenient to install on [travis-ci.org][], and gives an easy way to call `test/testall.sh`. -* `setup.py` runs the tests in a single version of Python. Also, packages the project for PyPI. -* `setup.cfg` configures `setup.py`. If you want to permanently skip a folder in testing, do it here. +```bash +make test +``` +or simply -## Testing +```bash +make +``` -### Code +The two commands above run all the available tests on all versions of Python supported. -To run the tests in the `test` directory, run +Running all tests requires that all the supported versions of Python installed. +These can be viewed with `make env`. - make test +### Single version of Python -or simply +Run - make +```bash +make quicktest +``` -The two commands above run all the available tests on all versions of Python supported. You should run `make` first, because it will also detect missing dependencies and install them. +## Cleaning -The first time you run `make`, you may be asked for your password. This is in order to install `pandoc`, a tool used for processing the README file. +To clean up the project directory, run -Running all tests requires that you have all the supported versions of Python installed. These are 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. Try to use your package manager to install them if possible. Otherwise, go to [python.org/downloads][] and install them manually. +```bash +make distclean +``` -#### Single version of Python +That should remove all the locally-installed dependencies and clear out all the temporary files. +You will need to restore the virtual envs with `make install-devtools`. -Run +To keep the virtual envs, but clean everything else, run - make quicktest +```bash +make clean +``` -### README +## Maintainers: Working with PyPI -To test changes to the README and make sure that they are compatible with PyPI's very restrictive rules, run +To publish a new version to Test PyPI, run - make rst +```bash +make upload +``` -`make rst` is also run by `make test`, but `make test` takes longer. +This will run `make test`, +build the source and wheel distributions, +and push them to the PyPI test server. +You can test your deploy in a fresh virtual env: -## Cleaning +```bash +python -m venv venv/testpypi +source venv/testpypi/bin/activate +make install-testpypi +python -c 'from intervaltree import IntervalTree; IntervalTree()' +``` -To clean up the project directory, run - - make distclean - -That should remove all the locally-installed dependencies and clear out all the temporary files. +If this looks like it went well, run -To keep the dependencies, but clean everything else, run +```bash +make prod upload +``` - make clean +to push the distribution to the production PyPI server. +You can delete your virtual env when you are done with it: -## Maintainers: Working with PyPI +```bash +rm -rf venv/testpypi +``` -### README +## Project structure -To update the README on PyPI, run +### `intervaltree` - make register +The `intervaltree` directory has three main files: -This will test the README's syntax strictly and push it up to the PyPI test server. - -If you are satisfied with the results, run +* `intervaltree.py` +* `interval.py` +* `node.py` - make release register +`intervaltree.py` and `interval.py` contain the public API to `IntervalTree` and `Interval`. `node.py` contains the internal logic of the tree. For the theory of how this type of tree works, read the following: -to do it for real on the production PyPI server. +* Wikipedia's [Interval tree][Wiki intervaltree] article +* Eternally Confuzzled's tutorial on [AVL balancing][Confuzzled AVL tree] +* Tyler Kahn's simpler, immutable [interval tree implementation][Kahn intervaltree] in Python -### Publishing +### `test` -To publish a new version to PyPI, run +All files ending with `_test.py` are detected and run whenever you run `make` or `make quicktest`. In those files, only functions beginning with `test_` are executed. - make upload +#### `test/data` +Some tests depend on having certain lists of `Interval`s. These are stored in the modules of `test/data`. Most of these modules only contain a `data` attribute, which is a list of tuples that is converted to a list of `Interval`s by `test/intervals.py`. You can access them by importing the dict of lists of `Interval`s `test.intervals.ivs`. -This will run `make test`, zip up the source distribution and push it to the PyPI test server. +Other tests (like `test/issue25_test.py`) depend on having pre-constructed `IntervalTree`s. These are constructed by `test/intervaltrees.py` and can be accessed by importing `test.intervaltrees.trees`. This is a dict of callables that return `IntervalTree`s. -If this looks like it went well, run +### Documentation files - make release upload +* `HACKING.md` is this file. +* `README.md` contains public API documentation and credits. +* `CHANGELOG.md` +* `LICENSE.txt` -to push the distribution to the production PyPI server. +### Other code files +* `Makefile` contains convenience routines for managing and testing the project. +* `pyproject.toml` configures how `intervaltree` gets built and deployed. [brew]: http://brew.sh/ +[install pyenv]: https://github.com/pyenv/pyenv/blob/master/README.md#installation [python.org/downloads]: http://www.python.org/downloads -[travis-ci.org]: https://travis-ci.org/ [Confuzzled AVL tree]: http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_avl.aspx [Wiki intervaltree]: http://en.wikipedia.org/wiki/Interval_tree [Kahn intervaltree]: http://zurb.com/forrst/posts/Interval_Tree_implementation_in_python-e0K diff --git a/Makefile b/Makefile index 8c3d1ed..1a55263 100644 --- a/Makefile +++ b/Makefile @@ -9,25 +9,32 @@ TEMPS=$(shell \ PYTHON_MINORS:=$(shell echo 2.7 3.{5..14}) PYTHONS:=$(shell echo $(PYTHON_MINORS) | xargs -n1 pyenv latest -k) +# MAINPY and MAINPYMINOR are the latest python in the list. +# make install-devtools creates two venvs: +# - mainpy$(MAINPYMINOR), and +# - py$(MAINPYMINOR). +# mainpy is used for uploads to pypi. py is for running tests. +# All targets using source venv/... must ensure +# that subsequent lines are run using in the same shell. MAINPY=$(shell echo $(PYTHONS) | tr ' ' '\n' | tail -n1) MAINPYMINOR=$(shell echo $(PYTHON_MINORS) | tr ' ' '\n' | tail -n1) -# PyPI server name, as specified in ~/.pypirc -# See http://peterdowns.com/posts/first-time-with-pypi.html PYPI=pypitest # default target -all: test +all: test build test: pytest flake8 quicktest: - source venv/mainpy$(MAINPY)/bin/activate + source venv/py$(MAINPYMINOR)/bin/activate || true; \ python -m pytest coverage: - pytest --cov=intervaltree - coverage html | sed -E 's@(Wrote HTML report to) (.*)@\1 file://'"$$PWD"'/\2@' + source venv/py$(MAINPYMINOR)/bin/activate || true; \ + python -m pytest --cov=intervaltree; \ + python -m coverage html | \ + sed -E 's@(Wrote HTML report to) (.*)@\1 file:/'"$$PWD"'/\2@' pytest: unset anyerr; \ @@ -50,13 +57,22 @@ pytest: anyerr=y; \ continue; \ fi; \ + deactivate; \ done; \ if [ -n "$$anyerr" ]; then exit 1; fi flake8: - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --extend-exclude=venv,requirements,dist - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + source venv/py$(MAINPYMINOR)/bin/activate || true; \ + python -m flake8 \ + --count --statistics --select=E9,F63,F7,F82 --show-source \ + --extend-exclude=venv,requirements,dist \ + .; \ + # python -m flake8 \ + # --count --statistics --exit-zero \ + # --max-complexity=10 --max-line-length=127 \ + # --extend-exclude=venv,requirements,dist \ + # .; \ + deactivate clean: clean-build clean-eggs clean-temps @@ -74,6 +90,7 @@ clean-env: clean-temps: rm -rf $(TEMPS) +# Project install/uninstall targets install-testpypi: pip install \ --no-cache-dir \ @@ -90,45 +107,57 @@ install-develop: uninstall: pip uninstall intervaltree sortedcontainers -# Setup for live upload -release: +# Setup for production upload +prod: $(eval PYPI=pypi) -# Build dist distribution +# Build wheel and sdist distribution build: clean - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - python$(MAINPYMINOR) -m hatch build + source venv/mainpy$(MAINPYMINOR)/bin/activate; \ + python$(MAINPYMINOR) -m hatch build; \ + deactivate upload: test build - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 + source venv/mainpy$(MAINPYMINOR)/bin/activate; \ if [[ "$(PYPI)" == pypitest ]]; then \ - python$(MAINPYMINOR) -m twine upload --verbose -r testpypi dist/*; \ + python$(MAINPYMINOR) -m twine upload --verbose \ + --repository testpypi \ + dist/*; \ else \ - python$(MAINPYMINOR) -m twine upload --verbose dist/*; \ - fi + python$(MAINPYMINOR) -m twine upload --verbose \ + dist/*; \ + fi; \ + deactivate install-devtools: pyenv-install-main-env pyenv-install-envs pyenv-is-installed: pyenv --version &>/dev/null || (echo "ERROR: pyenv not installed" && false) +# Set up an environment for running upload commands. pyenv-install-main-env: pyenv-is-installed @# echo N: Do not recompile a python interpreter if it is already present. echo N | pyenv install $(MAINPY) || true @echo "Setting up venv for main Python version $(MAINPY)" export PYENV_VERSION=$(MAINPY) unset CFLAGS; - python$(MAINPYMINOR) -m venv venv/mainpy$(MAINPYMINOR) || exit 1 - source venv/mainpy$(MAINPYMINOR)/bin/activate || exit 1 - pip$(MAINPYMINOR) install -U pip || exit 1 - pip$(MAINPYMINOR) install -r requirements/flake8.txt - pip$(MAINPYMINOR) install -r requirements/pytest.txt || exit 1 - pip$(MAINPYMINOR) install -U hatch hatchling twine - @echo "Finished setting up venv for main Python $(MAINPY)" - + python$(MAINPYMINOR) -m venv venv/mainpy$(MAINPYMINOR) + source venv/mainpy$(MAINPYMINOR)/bin/activate; \ + pip$(MAINPYMINOR) install -U pip; \ + pip$(MAINPYMINOR) install -U hatch twine; \ + echo "Finished setting up venv for main Python $(MAINPY)"; \ + deactivate + +# Set up environments for running tests. pyenv-install-envs: pyenv-is-installed @# echo N: Do not recompile a python interpreter if it is already present. - for pyver in $(PYTHONS); do echo N | pyenv install $$pyver || true; done + for pyver in $(PYTHONS); do \ + unset CFLAGS; \ + if [[ "$$pyver" == 2.* ]]; then \ + export CFLAGS=" -std=c17"; \ + fi; \ + echo N | pyenv install $$pyver || true; \ + done @# Setup virtual environments. @# venv only works on Python 3.3 and later. It will not work for 2.7. @# So, use virtualenv for 2.7. @@ -137,46 +166,45 @@ pyenv-install-envs: pyenv-is-installed echo "Setting up venv for Python $${pyver}"; \ export PYENV_VERSION=$$pyver; \ pyminor=$${pyver%.*}; \ - unset CFLAGS; \ if [[ "$$pyver" == 2.* ]]; then \ - export CFLAGS=" -std=c17"; \ pip$${pyminor} install virtualenv || continue; \ python$${pyminor} -m virtualenv \ --python=python$${pyminor} \ venv/py$${pyminor} || continue; \ - source venv/py$${pyminor}/bin/activate || continue; \ else \ python$${pyminor} -m venv venv/py$${pyminor} || continue; \ - source venv/py$${pyminor}/bin/activate || continue; \ fi; \ + source venv/py$${pyminor}/bin/activate || continue; \ pip$${pyminor} install -U pip || continue; \ pip$${pyminor} install -r requirements/pytest.txt || continue; \ + if [[ "$${pyminor}" == "$(MAINPYMINOR)" ]]; then \ + pip$${pyminor} install -r requirements/flake8.txt || continue; \ + fi; \ echo "Finished setting up venv for Python $${pyver}"; \ + deactivate \ done -pyenv-uninstall-envs: - rm -rf venv - pyenv-uninstall-versions: for pyver in $(PYTHONS); do (echo y | pyenv uninstall $$pyver) || true; done # for debugging the Makefile env: @echo - @echo TEMPS="\"$(TEMPS)\"" - @echo PYTHONS="\"$(PYTHONS)\"" - @echo PYTHON_MINORS="\"$(PYTHON_MINORS)\"" @echo MAINPY="\"$(MAINPY)\"" @echo MAINPYMINOR="\"$(MAINPYMINOR)\"" - @echo PYENV_VERSION="\"$(PYENV_VERSION)\"" + @echo PYTHONS="\"$(PYTHONS)\"" + @echo PYTHON_MINORS="\"$(PYTHON_MINORS)\"" + @echo CFLAGS="\"${CFLAGS}\"" + @echo TEMPS="\"$(TEMPS)\"" + @echo PYENV_VERSION="\"$${PYENV_VERSION}\"" @echo PYPI="\"$(PYPI)\"" - .PHONY: all \ build \ clean \ clean-build \ clean-eggs \ + clean-env \ clean-temps \ coverage \ distclean \ @@ -186,15 +214,13 @@ env: install-devtools \ install-pypi \ install-testpypi \ + prod \ pyenv-install-envs \ pyenv-install-main-env \ pyenv-is-installed \ - pyenv-uninstall-envs \ pyenv-uninstall-versions \ pytest \ quicktest \ - release \ test \ uninstall \ upload - From aebf938862a84407d4abb60a9c2f5bc5fc2ec646 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 20:08:28 -0700 Subject: [PATCH 08/10] build: fix version for test.pypi.org --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bbd8af3..b60193d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" [tool.uv-dynamic-versioning] -dirty = true pattern = "default-unprefixed" +format-jinja = "{{ base }}{% if distance > 0%}.post{{ distance }}.{{ commit }}{% endif %}{% if dirty %}+dirty{% endif %}" [tool.hatch.version] source = "uv-dynamic-versioning" From c71c60bd516f861bd7f2a305fb5c988a0e19acc0 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 20:27:44 -0700 Subject: [PATCH 09/10] build: disable commit hash in version --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b60193d..5509a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "hatchling.build" [tool.uv-dynamic-versioning] pattern = "default-unprefixed" -format-jinja = "{{ base }}{% if distance > 0%}.post{{ distance }}.{{ commit }}{% endif %}{% if dirty %}+dirty{% endif %}" +# test.pypi.org won't accept anything after a +, so metadata has to go :( +metadata = false [tool.hatch.version] source = "uv-dynamic-versioning" From e565e3d5b05fa94ff35b9aea2cb4d6f468917e56 Mon Sep 17 00:00:00 2001 From: Chaim Halbert Date: Tue, 23 Dec 2025 21:12:45 -0700 Subject: [PATCH 10/10] doc: update CHANGELOG.md for 3.2.1 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca8ec4..9212d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,22 @@ # Change log +## Version 3.2.1 +- Fixed: + - Build system includes sortedcontainers dependency in the wheel again +- Maintainers: + - Local testing happens inside virtual environments, + instead of in the system-installed Python environment. + - Add uv-dynamic-versioning. + This generates automatic version numbers for test.pypi.org, + based on the distance from the last tag version. + - CI tests "pip install ." + - Updated Makefile + - Updated HACKING.md + ## Version 3.2.0 +BROKEN: missing sortedcontainers dependency from wheel - Added: + - Add support for Python 3.9 - 3.14 - `pyproject.toml`, which modernizes the build and dependency system. - `requirements/*.txt`: If you are not using `pyproject.toml`, use `pip install -r` on `requirements/common.txt`