Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 92 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,19 @@ jobs:
matrix:
# ubuntu: full python range x both resolutions
os: [ubuntu-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.11", "3.12", "3.13", "3.14"]
resolution: [lowest-direct, highest]
# windows/macos: only the endpoints, highest resolution
include:
- { os: windows-latest, python-version: "3.10", resolution: highest }
- { os: windows-latest, python-version: "3.11", resolution: highest }
- { os: windows-latest, python-version: "3.14", resolution: highest }
- { os: macos-latest, python-version: "3.10", resolution: highest }
- { os: macos-latest, python-version: "3.11", resolution: highest }
- { os: macos-latest, python-version: "3.14", resolution: highest }

env:
UV_RESOLUTION: ${{ matrix.resolution }}
# a build that silently falls back to pure-Python must fail, not go green
SPATIAL_GRAPH_REQUIRE_PREBUILT: "1"

steps:
- uses: actions/checkout@v4
Expand All @@ -48,8 +50,12 @@ jobs:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
# --no-editable so we test the built wheel, prebuilt rtree modules and all,
# rather than an editable install of src/
- name: Install as a built wheel
run: uv sync --no-dev --group test --no-editable
- name: Test with coverage
run: uv run --no-dev --group test pytest -v --cov=spatial_graph --cov-report=xml
run: uv run --no-sync pytest -v --cov=spatial_graph --cov-report=xml
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -66,16 +72,89 @@ jobs:
enable-cache: true

- name: install
run: uv sync --no-dev --group test-codspeed
run: uv sync --no-dev --group test-codspeed --no-editable

- name: Run benchmarks
uses: CodSpeedHQ/action@v3
with:
run: uv run pytest -W ignore --codspeed -v --color=yes

# One abi3 wheel per platform, covering every supported CPython. Also the only
# thing that produces PyPI-acceptable manylinux tags -- `uv build` alone emits
# `linux_x86_64`, which PyPI rejects.
build-wheels:
name: Wheels ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest # manylinux + musllinux x86_64
- ubuntu-24.04-arm # manylinux + musllinux aarch64
- windows-latest # win_amd64 + win32
- macos-15-intel # macOS x86_64
- macos-latest # macOS arm64
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # setuptools-scm needs the tags
- uses: pypa/cibuildwheel@v4.1.1
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}
path: wheelhouse/*.whl

build-sdist:
name: Sdist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
- run: uv build --sdist
- uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz

# Everything else builds its own wheel; this is the only job that exercises
# the artifacts we would actually publish, and the only one that checks the
# stable-ABI claim -- one cp311 wheel running on every supported CPython.
test-abi3-wheel:
name: abi3 wheel on py${{ matrix.python-version }}
needs: build-wheels
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
env:
SPATIAL_GRAPH_REQUIRE_PREBUILT: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: wheels-ubuntu-latest
path: wheelhouse
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
# Name the wheel directly: cibuildwheel emits a manylinux and a musllinux
# wheel, so a bare *.whl glob is ambiguous, while installing by package
# name would resolve `spatial-graph` from PyPI instead of this artifact.
- name: Install the built wheel with its real dependencies
run: |
uv venv
uv pip install wheelhouse/*manylinux*.whl pytest
- name: Test it
# test_prebuilt.py asserts the real property directly: constructing a
# prebuilt variant must not reach witty.compile_cython
run: uv run --no-sync pytest tests/test_prebuilt.py -v

deploy:
name: Deploy
needs: test
needs: [test, build-wheels, build-sdist, test-abi3-wheel]
if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule'
runs-on: ubuntu-latest

Expand All @@ -85,16 +164,15 @@ jobs:

steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
pattern: wheels-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@v4
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"

- name: 👷 Build
run: uv build
name: sdist
path: dist

- name: 🚢 Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ where each node has an nD position (in time or space).
* attribute access
* minimal memory footprint
* minimal dependencies
* `cython` / `witty` / `cheetah3` for runtime compilation
* `cython` / `witty` / `cheetah3`, used only when something has to be
compiled at runtime (see Cross-Platform Support)
* numpy for array interfaces
* PYX API for graph algorithms in C/C++

Expand Down Expand Up @@ -116,16 +117,34 @@ A `SpatialGraph` consists of three data structures:

## Cross-Platform Support

`spatial_graph` compiles C/C++ code at runtime, and as such needs access to a
compiler. If you already have one, great! You can use the PyPI package.
`spatial_graph` generates specialized C/C++ for the exact data types you ask
for. Where those types can be known in advance we compile them ahead of time
and ship them in the wheels; everything else is compiled on your machine the
first time it is used, which needs a C compiler.

If you (or your users) don't have a compiler installed, you either need to
**No compiler needed.** The PyPI wheels contain prebuilt `PointRTree` and
`LineRTree` variants for the common combinations: `float32`/`float64`
coordinates, 2 to 5 dimensions, and `int64`/`uint64` items -- as `int64[2]` /
`uint64[2]` for `LineRTree`, whose items are node pairs. If your R-tree matches
one of those -- as most do -- nothing is compiled, on any supported Python.

1. Install a compiler. This might be weird for non-technical users.
2. Install `spatial_graph` from `conda-forge`, where we include a compiler
(`clang`) in its dependencies.
**Compiler needed.** Two cases fall back to compiling at runtime:

### Why is this so complicated?
1. `Graph`, `DiGraph`, `SpatialGraph` and `SpatialDiGraph`. Their node and edge
attribute types are only known when you construct the graph, so they cannot
be enumerated ahead of time.
2. R-trees outside the prebuilt set above (an `int32` item type, say, or 6
dimensions).

If you or your users need those without a compiler, you can still install
`spatial_graph` from `conda-forge`, where we include a compiler (`clang`) in
its dependencies.

The wheels are `abi3` (stable ABI) and require Python 3.11 or newer, so one
wheel per platform covers every supported CPython. Python 3.10 users should
pin to a release before this one.

### Why can't everything be prebuilt?

There is no cross-platform C/C++ compiler that we can install using `pip`.
[`numba`](https://github.com/numba/numba) is maybe the closest to having solved
Expand Down
46 changes: 35 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
requires = [
"setuptools>=77",
"setuptools-scm>=8",
"Cython>=3.1",
"CT3>=3.3.3",
"numpy", # imported (not linked) while rendering the wrappers
]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]

[tool.hatch.version]
source = "vcs"
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
# The JIT fallback compiles from these at runtime, so they must ship in the
# wheel. `include-package-data` (on by default here) would pick them up from
# the VCS/sdist manifest anyway; listing them keeps that explicit and works
# even when no file finder is available. Patterns are package-relative, so the
# vendored C under `_rtree/src/` and `_graph/src/` needs the `src/` prefix.
"*" = ["py.typed", "*.pyx", "src/*.c", "src/*.h", "src/LICENSE*", "src/*.md"]

[project]
name = "spatial-graph"
dynamic = ["version"]
description = "A spatial graph datastructure for python."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
requires-python = ">=3.11"
license = "MIT"
license-files = ["LICENSE"]
authors = [
{ email = "funkej@janelia.hhmi.org", name = "Jan Funke" },
{ email = "talley.lambert@gmail.com", name = "Talley Lambert" },
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand All @@ -33,8 +48,7 @@ dependencies = [
"numpy>=2.3.2; python_version >= '3.14'",
"numpy>=2.1.0; python_version >= '3.13'",
"numpy>=1.26.0; python_version >= '3.12'",
"numpy>=1.23.2; python_version >= '3.11'",
"numpy>=1.21.2",
"numpy>=1.23.2",
"setuptools>=75.8.0",
"typing_extensions>=4.5.0", # witty<=0.3.1 imports it without declaring it
]
Expand Down Expand Up @@ -63,8 +77,18 @@ docs = [
homepage = "https://github.com/funkelab/spatial_graph"
repository = "https://github.com/funkelab/spatial_graph"

[tool.cibuildwheel]
# a single abi3 build per platform covers every supported CPython
build = "cp311-*"
# never let a wheel silently degrade to pure Python
environment = { SPATIAL_GRAPH_REQUIRE_PREBUILT = "1" }
test-groups = ["test"]
# these exercise the prebuilt modules in the repaired wheel without needing a
# compiler; cross-version and numpy-only checks live in the CI workflow
test-command = "pytest {project}/tests/test_prebuilt.py -q"

[tool.ruff]
target-version = "py310"
target-version = "py311"
line-length = 88
fix = true
unsafe-fixes = true
Expand Down
Loading
Loading