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 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)' 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/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` 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 868c1e0..1a55263 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,52 +7,90 @@ 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 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 -TWINE=twine # default target -all: test +all: test build -test: pytest +test: pytest flake8 quicktest: - PYPI=$(PYPI) python setup.py test + source venv/py$(MAINPYMINOR)/bin/activate || true; \ + python -m pytest coverage: - coverage run --source=intervaltree setup.py develop test - coverage report - coverage html + 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: 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; \ + deactivate; \ + done; \ + if [ -n "$$anyerr" ]; then exit 1; fi + +flake8: + 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 -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) +# Project install/uninstall targets install-testpypi: pip install \ --no-cache-dir \ @@ -66,90 +102,125 @@ 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: +# Setup for production upload +prod: $(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 wheel and sdist distribution +build: clean + source venv/mainpy$(MAINPYMINOR)/bin/activate; \ + python$(MAINPYMINOR) -m hatch build; \ + deactivate -dist-upload: sdist-build bdist-build +upload: test build + source venv/mainpy$(MAINPYMINOR)/bin/activate; \ if [[ "$(PYPI)" == pypitest ]]; then \ - $(TWINE) upload --repository-url https://test.pypi.org/legacy/ dist/*; \ + python$(MAINPYMINOR) -m twine upload --verbose \ + --repository testpypi \ + dist/*; \ else \ - $(TWINE) upload dist/*; \ - fi + python$(MAINPYMINOR) -m twine upload --verbose \ + dist/*; \ + fi; \ + deactivate -deps-dev: pyenv-install-versions - -# Uploads to test server, unless the release target was run too -upload: test clean dist-upload +install-devtools: pyenv-install-main-env pyenv-install-envs 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 +# 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) + 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 \ + 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. 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%.*}; \ + if [[ "$$pyver" == 2.* ]]; then \ + pip$${pyminor} install virtualenv || continue; \ + python$${pyminor} -m virtualenv \ + --python=python$${pyminor} \ + venv/py$${pyminor} || continue; \ + else \ + python$${pyminor} -m venv venv/py$${pyminor} || 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-versions: + for pyver in $(PYTHONS); do (echo y | pyenv uninstall $$pyver) || true; done # for debugging the Makefile env: @echo - @echo TEMPS="\"$(TEMPS)\"" + @echo MAINPY="\"$(MAINPY)\"" + @echo MAINPYMINOR="\"$(MAINPYMINOR)\"" @echo PYTHONS="\"$(PYTHONS)\"" - @echo PYTHON_MAJORS="\"$(PYTHON_MAJORS)\"" @echo PYTHON_MINORS="\"$(PYTHON_MINORS)\"" + @echo CFLAGS="\"${CFLAGS}\"" + @echo TEMPS="\"$(TEMPS)\"" + @echo PYENV_VERSION="\"$${PYENV_VERSION}\"" @echo PYPI="\"$(PYPI)\"" - .PHONY: all \ - test \ - quicktest \ - pytest \ + build \ clean \ - distclean \ clean-build \ clean-eggs \ + clean-env \ clean-temps \ - install-testpypi \ - install-pypi \ + coverage \ + distclean \ + env \ + flake8 \ install-develop \ - pyenv-install-versions \ + install-devtools \ + install-pypi \ + install-testpypi \ + prod \ + pyenv-install-envs \ + pyenv-install-main-env \ pyenv-is-installed \ + pyenv-uninstall-versions \ + pytest \ + quicktest \ + test \ uninstall \ - register \ - release \ - sdist-upload \ - deps-ci \ - deps-dev \ - pm-update \ - upload \ - env - + upload diff --git a/pyproject.toml b/pyproject.toml index 17312cb..5509a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,19 @@ +[build-system] +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" + +[tool.uv-dynamic-versioning] +pattern = "default-unprefixed" +# test.pypi.org won't accept anything after a +, so metadata has to go :( +metadata = false + +[tool.hatch.version] +source = "uv-dynamic-versioning" + [project] name = "intervaltree" -description='Editable interval tree data structure for Python 2 and 3' -version = "3.2.0" +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" }, @@ -68,9 +80,3 @@ norecursedirs = [ '.tox', '.vscode', ] - -[tool.hatch] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build"