spatial-graph 0.0.6__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. spatial_graph-0.1.0/.github/workflows/ci.yml +202 -0
  2. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.pre-commit-config.yaml +2 -2
  3. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/PKG-INFO +41 -24
  4. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/README.md +27 -8
  5. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/pyproject.toml +36 -12
  6. spatial_graph-0.1.0/setup.cfg +4 -0
  7. spatial_graph-0.1.0/setup.py +188 -0
  8. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/graph_base.py +10 -4
  9. spatial_graph-0.1.0/src/spatial_graph/_rtree/_codegen.py +72 -0
  10. spatial_graph-0.1.0/src/spatial_graph/_rtree/_naming.py +63 -0
  11. spatial_graph-0.1.0/src/spatial_graph/_rtree/_prebuilt/__init__.py +5 -0
  12. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/rtree.py +41 -28
  13. spatial_graph-0.1.0/src/spatial_graph.egg-info/PKG-INFO +209 -0
  14. spatial_graph-0.1.0/src/spatial_graph.egg-info/SOURCES.txt +54 -0
  15. spatial_graph-0.1.0/src/spatial_graph.egg-info/dependency_links.txt +1 -0
  16. spatial_graph-0.1.0/src/spatial_graph.egg-info/requires.txt +14 -0
  17. spatial_graph-0.1.0/src/spatial_graph.egg-info/scm_file_list.json +51 -0
  18. spatial_graph-0.1.0/src/spatial_graph.egg-info/scm_version.json +8 -0
  19. spatial_graph-0.1.0/src/spatial_graph.egg-info/top_level.txt +1 -0
  20. spatial_graph-0.1.0/tests/test_prebuilt.py +97 -0
  21. spatial_graph-0.0.6/.github/workflows/ci.yml +0 -124
  22. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.github/workflows/docs.yaml +0 -0
  23. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.gitignore +0 -0
  24. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/Dockerfile +0 -0
  25. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/LICENSE +0 -0
  26. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/docs/index.md +0 -0
  27. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/examples/basic_usage.py +0 -0
  28. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/examples/query_nearest_vispy.py +0 -0
  29. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/mkdocs.yml +0 -0
  30. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/__init__.py +0 -0
  31. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_dtypes.py +0 -0
  32. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/__init__.py +0 -0
  33. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/graph.py +0 -0
  34. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/src/LICENSE.txt +0 -0
  35. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/src/graph_lite.h +0 -0
  36. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/views.py +0 -0
  37. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/wrapper_template.pyx +0 -0
  38. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/__init__.py +0 -0
  39. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/line_rtree.py +0 -0
  40. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/point_rtree.py +0 -0
  41. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/ARCHITECTURE.md +0 -0
  42. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/LICENSE +0 -0
  43. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/config.h +0 -0
  44. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/rtree.c +0 -0
  45. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/rtree.h +0 -0
  46. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/tree_base.py +0 -0
  47. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/wrapper_template.pyx +0 -0
  48. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_spatial_graph.py +0 -0
  49. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_util.py +0 -0
  50. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/py.typed +0 -0
  51. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_attributes.py +0 -0
  52. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_bench.py +0 -0
  53. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_dtype.py +0 -0
  54. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_graph.py +0 -0
  55. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_invalid_inputs.py +0 -0
  56. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_rtree.py +0 -0
  57. {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_spatial_graph.py +0 -0
@@ -0,0 +1,202 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+ tags: [v*]
9
+ workflow_dispatch:
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ lint:
17
+ name: Lint
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - name: Run pre-commit
22
+ run: pipx run pre-commit run --all-files
23
+
24
+ test:
25
+ name: ${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.resolution }}
26
+ runs-on: ${{ matrix.os }}
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ # ubuntu: full python range x both resolutions
31
+ os: [ubuntu-latest]
32
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
33
+ resolution: [lowest-direct, highest]
34
+ # windows/macos: only the endpoints, highest resolution
35
+ include:
36
+ - { os: windows-latest, python-version: "3.11", resolution: highest }
37
+ - { os: windows-latest, python-version: "3.14", resolution: highest }
38
+ - { os: macos-latest, python-version: "3.11", resolution: highest }
39
+ - { os: macos-latest, python-version: "3.14", resolution: highest }
40
+
41
+ env:
42
+ UV_RESOLUTION: ${{ matrix.resolution }}
43
+ # a build that silently falls back to pure-Python must fail, not go green
44
+ SPATIAL_GRAPH_REQUIRE_PREBUILT: "1"
45
+
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+ - uses: astral-sh/setup-uv@v6
49
+ with:
50
+ python-version: ${{ matrix.python-version }}
51
+ enable-cache: true
52
+ cache-dependency-glob: "**/pyproject.toml"
53
+ # --no-editable so we test the built wheel, prebuilt rtree modules and all,
54
+ # rather than an editable install of src/
55
+ - name: Install as a built wheel
56
+ run: uv sync --no-dev --group test --no-editable
57
+ - name: Test with coverage
58
+ run: uv run --no-sync pytest -v --cov=spatial_graph --cov-report=xml
59
+ - uses: codecov/codecov-action@v5
60
+ with:
61
+ token: ${{ secrets.CODECOV_TOKEN }}
62
+
63
+ benchmarks:
64
+ runs-on: ubuntu-latest
65
+ env:
66
+ UV_NO_SYNC: "1"
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: astral-sh/setup-uv@v6
70
+ with:
71
+ python-version: "3.13"
72
+ enable-cache: true
73
+
74
+ - name: install
75
+ run: uv sync --no-dev --group test-codspeed --no-editable
76
+
77
+ - name: Run benchmarks
78
+ uses: CodSpeedHQ/action@v3
79
+ with:
80
+ run: uv run pytest -W ignore --codspeed -v --color=yes
81
+
82
+ # One abi3 wheel per platform, covering every supported CPython. Also the only
83
+ # thing that produces PyPI-acceptable manylinux tags -- `uv build` alone emits
84
+ # `linux_x86_64`, which PyPI rejects.
85
+ build-wheels:
86
+ name: Wheels ${{ matrix.os }}
87
+ runs-on: ${{ matrix.os }}
88
+ strategy:
89
+ fail-fast: false
90
+ matrix:
91
+ os:
92
+ - ubuntu-latest # manylinux + musllinux x86_64
93
+ - ubuntu-24.04-arm # manylinux + musllinux aarch64
94
+ - windows-latest # win_amd64 + win32
95
+ - macos-15-intel # macOS x86_64
96
+ - macos-latest # macOS arm64
97
+ steps:
98
+ - uses: actions/checkout@v4
99
+ with:
100
+ fetch-depth: 0 # setuptools-scm needs the tags
101
+ - uses: pypa/cibuildwheel@v4.1.1
102
+ - uses: actions/upload-artifact@v4
103
+ with:
104
+ name: wheels-${{ matrix.os }}
105
+ path: wheelhouse/*.whl
106
+
107
+ build-sdist:
108
+ name: Sdist
109
+ runs-on: ubuntu-latest
110
+ steps:
111
+ - uses: actions/checkout@v4
112
+ with:
113
+ fetch-depth: 0
114
+ - uses: astral-sh/setup-uv@v6
115
+ - run: uv build --sdist
116
+ - uses: actions/upload-artifact@v4
117
+ with:
118
+ name: sdist
119
+ path: dist/*.tar.gz
120
+
121
+ # Everything else builds its own wheel; this is the only job that exercises
122
+ # the artifacts we would actually publish, and the only one that checks the
123
+ # stable-ABI claim -- one cp311 wheel running on every supported CPython.
124
+ test-abi3-wheel:
125
+ name: abi3 wheel on py${{ matrix.python-version }}
126
+ needs: build-wheels
127
+ runs-on: ubuntu-latest
128
+ strategy:
129
+ fail-fast: false
130
+ matrix:
131
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
132
+ env:
133
+ SPATIAL_GRAPH_REQUIRE_PREBUILT: "1"
134
+ steps:
135
+ - uses: actions/checkout@v4
136
+ - uses: actions/download-artifact@v4
137
+ with:
138
+ name: wheels-ubuntu-latest
139
+ path: wheelhouse
140
+ - uses: astral-sh/setup-uv@v6
141
+ with:
142
+ python-version: ${{ matrix.python-version }}
143
+ # Name the wheel directly: cibuildwheel emits a manylinux and a musllinux
144
+ # wheel, so a bare *.whl glob is ambiguous, while installing by package
145
+ # name would resolve `spatial-graph` from PyPI instead of this artifact.
146
+ - name: Install the built wheel with its real dependencies
147
+ run: |
148
+ uv venv
149
+ uv pip install wheelhouse/*manylinux*.whl pytest
150
+ - name: Test it
151
+ # test_prebuilt.py asserts the real property directly: constructing a
152
+ # prebuilt variant must not reach witty.compile_cython
153
+ run: uv run --no-sync pytest tests/test_prebuilt.py -v
154
+
155
+ deploy:
156
+ name: Deploy
157
+ needs: [test, build-wheels, build-sdist, test-abi3-wheel]
158
+ if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule'
159
+ runs-on: ubuntu-latest
160
+
161
+ permissions:
162
+ id-token: write
163
+ contents: write
164
+
165
+ steps:
166
+ - uses: actions/checkout@v4
167
+ - uses: actions/download-artifact@v4
168
+ with:
169
+ pattern: wheels-*
170
+ path: dist
171
+ merge-multiple: true
172
+ - uses: actions/download-artifact@v4
173
+ with:
174
+ name: sdist
175
+ path: dist
176
+
177
+ - name: 🚢 Publish to PyPI
178
+ uses: pypa/gh-action-pypi-publish@release/v1
179
+ with:
180
+ password: ${{ secrets.PYPI_API_TOKEN }}
181
+
182
+ - uses: softprops/action-gh-release@v2
183
+ with:
184
+ generate_release_notes: true
185
+ files: "./dist/*"
186
+
187
+ docker-test:
188
+ name: Docker Test gxx${{ matrix.gxx_major }}
189
+ runs-on: ubuntu-latest
190
+ strategy:
191
+ fail-fast: false
192
+ matrix:
193
+ gxx_major: [14, 15]
194
+
195
+ steps:
196
+ - name: Checkout code
197
+ uses: actions/checkout@v4
198
+
199
+ - name: Build and test Docker image
200
+ run: |
201
+ docker build --build-arg GXX_MAJOR=${{ matrix.gxx_major }} -t spatial_graph .
202
+ docker run --rm spatial_graph
@@ -15,13 +15,13 @@ repos:
15
15
  - id: check-added-large-files
16
16
 
17
17
  - repo: https://github.com/astral-sh/ruff-pre-commit
18
- rev: v0.15.16
18
+ rev: v0.16.1
19
19
  hooks:
20
20
  - id: ruff-check
21
21
  args: [--fix, --unsafe-fixes]
22
22
  - id: ruff-format
23
23
 
24
24
  - repo: https://github.com/pre-commit/mirrors-mypy
25
- rev: v2.1.0
25
+ rev: v2.3.0
26
26
  hooks:
27
27
  - id: mypy
@@ -1,32 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spatial-graph
3
- Version: 0.0.6
3
+ Version: 0.1.0
4
4
  Summary: A spatial graph datastructure for python.
5
+ Author-email: Jan Funke <funkej@janelia.hhmi.org>, Talley Lambert <talley.lambert@gmail.com>
6
+ License-Expression: MIT
5
7
  Project-URL: homepage, https://github.com/funkelab/spatial_graph
6
8
  Project-URL: repository, https://github.com/funkelab/spatial_graph
7
- Author-email: Jan Funke <funkej@janelia.hhmi.org>, Talley Lambert <talley.lambert@gmail.com>
8
- License: MIT
9
- License-File: LICENSE
10
9
  Classifier: Development Status :: 3 - Alpha
11
10
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.10
14
11
  Classifier: Programming Language :: Python :: 3.11
15
12
  Classifier: Programming Language :: Python :: 3.12
16
13
  Classifier: Programming Language :: Python :: 3.13
17
14
  Classifier: Programming Language :: Python :: 3.14
18
15
  Classifier: Typing :: Typed
19
- Requires-Python: >=3.10
20
- Requires-Dist: ct3>=3.3.3
21
- Requires-Dist: numpy>=1.21.2
22
- Requires-Dist: numpy>=1.23.2; python_version >= '3.11'
23
- Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
24
- Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
25
- Requires-Dist: numpy>=2.3.2; python_version >= '3.14'
26
- Requires-Dist: setuptools>=75.8.0
27
- Requires-Dist: typing-extensions>=4.5.0
28
- Requires-Dist: witty>=v0.3.0
16
+ Requires-Python: >=3.11
29
17
  Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: witty>=0.3.2
20
+ Requires-Dist: CT3>=3.3.3
21
+ Requires-Dist: numpy>=2.3.2; python_version >= "3.14"
22
+ Requires-Dist: numpy>=2.1.0; python_version >= "3.13"
23
+ Requires-Dist: numpy>=1.26.0; python_version >= "3.12"
24
+ Requires-Dist: numpy>=1.23.2
25
+ Requires-Dist: setuptools>=75.8.0
26
+ Requires-Dist: typing_extensions>=4.5.0
27
+ Dynamic: license-file
30
28
 
31
29
  # spatial-graph
32
30
 
@@ -56,7 +54,8 @@ where each node has an nD position (in time or space).
56
54
  * attribute access
57
55
  * minimal memory footprint
58
56
  * minimal dependencies
59
- * `cython` / `witty` / `cheetah3` for runtime compilation
57
+ * `cython` / `witty` / `cheetah3`, used only when something has to be
58
+ compiled at runtime (see Cross-Platform Support)
60
59
  * numpy for array interfaces
61
60
  * PYX API for graph algorithms in C/C++
62
61
 
@@ -146,16 +145,34 @@ A `SpatialGraph` consists of three data structures:
146
145
 
147
146
  ## Cross-Platform Support
148
147
 
149
- `spatial_graph` compiles C/C++ code at runtime, and as such needs access to a
150
- compiler. If you already have one, great! You can use the PyPI package.
148
+ `spatial_graph` generates specialized C/C++ for the exact data types you ask
149
+ for. Where those types can be known in advance we compile them ahead of time
150
+ and ship them in the wheels; everything else is compiled on your machine the
151
+ first time it is used, which needs a C compiler.
152
+
153
+ **No compiler needed.** The PyPI wheels contain prebuilt `PointRTree` and
154
+ `LineRTree` variants for the common combinations: `float32`/`float64`
155
+ coordinates, 2 to 5 dimensions, and `int64`/`uint64` items -- as `int64[2]` /
156
+ `uint64[2]` for `LineRTree`, whose items are node pairs. If your R-tree matches
157
+ one of those -- as most do -- nothing is compiled, on any supported Python.
158
+
159
+ **Compiler needed.** Two cases fall back to compiling at runtime:
160
+
161
+ 1. `Graph`, `DiGraph`, `SpatialGraph` and `SpatialDiGraph`. Their node and edge
162
+ attribute types are only known when you construct the graph, so they cannot
163
+ be enumerated ahead of time.
164
+ 2. R-trees outside the prebuilt set above (an `int32` item type, say, or 6
165
+ dimensions).
151
166
 
152
- If you (or your users) don't have a compiler installed, you either need to
167
+ If you or your users need those without a compiler, you can still install
168
+ `spatial_graph` from `conda-forge`, where we include a compiler (`clang`) in
169
+ its dependencies.
153
170
 
154
- 1. Install a compiler. This might be weird for non-technical users.
155
- 2. Install `spatial_graph` from `conda-forge`, where we include a compiler
156
- (`clang`) in its dependencies.
171
+ The wheels are `abi3` (stable ABI) and require Python 3.11 or newer, so one
172
+ wheel per platform covers every supported CPython. Python 3.10 users should
173
+ pin to a release before this one.
157
174
 
158
- ### Why is this so complicated?
175
+ ### Why can't everything be prebuilt?
159
176
 
160
177
  There is no cross-platform C/C++ compiler that we can install using `pip`.
161
178
  [`numba`](https://github.com/numba/numba) is maybe the closest to having solved
@@ -26,7 +26,8 @@ where each node has an nD position (in time or space).
26
26
  * attribute access
27
27
  * minimal memory footprint
28
28
  * minimal dependencies
29
- * `cython` / `witty` / `cheetah3` for runtime compilation
29
+ * `cython` / `witty` / `cheetah3`, used only when something has to be
30
+ compiled at runtime (see Cross-Platform Support)
30
31
  * numpy for array interfaces
31
32
  * PYX API for graph algorithms in C/C++
32
33
 
@@ -116,16 +117,34 @@ A `SpatialGraph` consists of three data structures:
116
117
 
117
118
  ## Cross-Platform Support
118
119
 
119
- `spatial_graph` compiles C/C++ code at runtime, and as such needs access to a
120
- compiler. If you already have one, great! You can use the PyPI package.
120
+ `spatial_graph` generates specialized C/C++ for the exact data types you ask
121
+ for. Where those types can be known in advance we compile them ahead of time
122
+ and ship them in the wheels; everything else is compiled on your machine the
123
+ first time it is used, which needs a C compiler.
121
124
 
122
- If you (or your users) don't have a compiler installed, you either need to
125
+ **No compiler needed.** The PyPI wheels contain prebuilt `PointRTree` and
126
+ `LineRTree` variants for the common combinations: `float32`/`float64`
127
+ coordinates, 2 to 5 dimensions, and `int64`/`uint64` items -- as `int64[2]` /
128
+ `uint64[2]` for `LineRTree`, whose items are node pairs. If your R-tree matches
129
+ one of those -- as most do -- nothing is compiled, on any supported Python.
123
130
 
124
- 1. Install a compiler. This might be weird for non-technical users.
125
- 2. Install `spatial_graph` from `conda-forge`, where we include a compiler
126
- (`clang`) in its dependencies.
131
+ **Compiler needed.** Two cases fall back to compiling at runtime:
127
132
 
128
- ### Why is this so complicated?
133
+ 1. `Graph`, `DiGraph`, `SpatialGraph` and `SpatialDiGraph`. Their node and edge
134
+ attribute types are only known when you construct the graph, so they cannot
135
+ be enumerated ahead of time.
136
+ 2. R-trees outside the prebuilt set above (an `int32` item type, say, or 6
137
+ dimensions).
138
+
139
+ If you or your users need those without a compiler, you can still install
140
+ `spatial_graph` from `conda-forge`, where we include a compiler (`clang`) in
141
+ its dependencies.
142
+
143
+ The wheels are `abi3` (stable ABI) and require Python 3.11 or newer, so one
144
+ wheel per platform covers every supported CPython. Python 3.10 users should
145
+ pin to a release before this one.
146
+
147
+ ### Why can't everything be prebuilt?
129
148
 
130
149
  There is no cross-platform C/C++ compiler that we can install using `pip`.
131
150
  [`numba`](https://github.com/numba/numba) is maybe the closest to having solved
@@ -1,17 +1,34 @@
1
1
  [build-system]
2
- requires = ["hatchling", "hatch-vcs"]
3
- build-backend = "hatchling.build"
2
+ requires = [
3
+ "setuptools>=77",
4
+ "setuptools-scm>=8",
5
+ "Cython>=3.1",
6
+ "CT3>=3.3.3",
7
+ "numpy", # imported (not linked) while rendering the wrappers
8
+ ]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [tool.setuptools_scm]
4
12
 
5
- [tool.hatch.version]
6
- source = "vcs"
13
+ [tool.setuptools.packages.find]
14
+ where = ["src"]
15
+
16
+ [tool.setuptools.package-data]
17
+ # The JIT fallback compiles from these at runtime, so they must ship in the
18
+ # wheel. `include-package-data` (on by default here) would pick them up from
19
+ # the VCS/sdist manifest anyway; listing them keeps that explicit and works
20
+ # even when no file finder is available. Patterns are package-relative, so the
21
+ # vendored C under `_rtree/src/` and `_graph/src/` needs the `src/` prefix.
22
+ "*" = ["py.typed", "*.pyx", "src/*.c", "src/*.h", "src/LICENSE*", "src/*.md"]
7
23
 
8
24
  [project]
9
25
  name = "spatial-graph"
10
26
  dynamic = ["version"]
11
27
  description = "A spatial graph datastructure for python."
12
28
  readme = "README.md"
13
- requires-python = ">=3.10"
14
- license = { text = "MIT" }
29
+ requires-python = ">=3.11"
30
+ license = "MIT"
31
+ license-files = ["LICENSE"]
15
32
  authors = [
16
33
  { email = "funkej@janelia.hhmi.org", name = "Jan Funke" },
17
34
  { email = "talley.lambert@gmail.com", name = "Talley Lambert" },
@@ -19,8 +36,6 @@ authors = [
19
36
  classifiers = [
20
37
  "Development Status :: 3 - Alpha",
21
38
  "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
- "Programming Language :: Python :: 3.10",
24
39
  "Programming Language :: Python :: 3.11",
25
40
  "Programming Language :: Python :: 3.12",
26
41
  "Programming Language :: Python :: 3.13",
@@ -28,13 +43,12 @@ classifiers = [
28
43
  "Typing :: Typed",
29
44
  ]
30
45
  dependencies = [
31
- "witty>=v0.3.0",
46
+ "witty>=0.3.2",
32
47
  "CT3>=3.3.3",
33
48
  "numpy>=2.3.2; python_version >= '3.14'",
34
49
  "numpy>=2.1.0; python_version >= '3.13'",
35
50
  "numpy>=1.26.0; python_version >= '3.12'",
36
- "numpy>=1.23.2; python_version >= '3.11'",
37
- "numpy>=1.21.2",
51
+ "numpy>=1.23.2",
38
52
  "setuptools>=75.8.0",
39
53
  "typing_extensions>=4.5.0", # witty<=0.3.1 imports it without declaring it
40
54
  ]
@@ -63,8 +77,18 @@ docs = [
63
77
  homepage = "https://github.com/funkelab/spatial_graph"
64
78
  repository = "https://github.com/funkelab/spatial_graph"
65
79
 
80
+ [tool.cibuildwheel]
81
+ # a single abi3 build per platform covers every supported CPython
82
+ build = "cp311-*"
83
+ # never let a wheel silently degrade to pure Python
84
+ environment = { SPATIAL_GRAPH_REQUIRE_PREBUILT = "1" }
85
+ test-groups = ["test"]
86
+ # these exercise the prebuilt modules in the repaired wheel without needing a
87
+ # compiler; cross-version and numpy-only checks live in the CI workflow
88
+ test-command = "pytest {project}/tests/test_prebuilt.py -q"
89
+
66
90
  [tool.ruff]
67
- target-version = "py310"
91
+ target-version = "py311"
68
92
  line-length = 88
69
93
  fix = true
70
94
  unsafe-fixes = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,188 @@
1
+ """Compile RTree variants ahead of time into a stable-ABI (abi3) wheel.
2
+
3
+ Renders the same pyx wrappers the runtime would JIT-compile (via
4
+ `_rtree._codegen`) for every variant in `iter_specs()`, so the two paths are
5
+ generated from one source. One wheel per platform then covers every supported
6
+ CPython, and those variants are never compiled on the user's machine -- so no
7
+ C compiler is invoked for them.
8
+
9
+ Set `SPATIAL_GRAPH_NO_PREBUILT=1` to build a pure-Python wheel instead, or
10
+ `SPATIAL_GRAPH_REQUIRE_PREBUILT=1` (as CI does) to turn a failure to compile
11
+ into a hard error rather than a silent fall back to JIT.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ import tempfile
19
+ import warnings
20
+ from pathlib import Path
21
+
22
+ from setuptools import Extension, setup
23
+
24
+ ROOT = Path(__file__).parent
25
+ SRC = ROOT / "src"
26
+ PREBUILT_PKG = "spatial_graph._rtree._prebuilt"
27
+
28
+ # The wrappers pass numpy arrays as typed memoryviews, which compile to
29
+ # PyObject_GetBuffer/PyBuffer_Release. Those entered the limited API in 3.11
30
+ # (moved from cpython/object.h, excluded under Py_LIMITED_API, to pybuffer.h),
31
+ # so 3.11 is the floor for a stable-ABI build -- and matches requires-python.
32
+ ABI3_MIN = (3, 11)
33
+ ABI3_TAG = f"cp{ABI3_MIN[0]}{ABI3_MIN[1]}"
34
+ ABI3_HEX = f"0x{ABI3_MIN[0]:02x}{ABI3_MIN[1]:02x}0000"
35
+
36
+ WIN = sys.platform == "win32"
37
+
38
+
39
+ def _src_on_path() -> None:
40
+ """Make the package under `src/` importable, for the helpers shared with it.
41
+
42
+ Callers import `spatial_graph` only once they know they need it: importing
43
+ it at module scope would make every command -- `sdist` and `egg_info`
44
+ included -- fail if anything the package imports is missing from the build
45
+ environment.
46
+ """
47
+ if str(SRC) not in sys.path:
48
+ sys.path.insert(0, str(SRC))
49
+
50
+
51
+ def prebuilt_extensions() -> list[Extension]:
52
+ """Render every prebuilt RTree variant and declare it as an extension."""
53
+ from Cython.Build import cythonize
54
+
55
+ _src_on_path()
56
+ from spatial_graph._rtree._codegen import build_wrapper, iter_specs
57
+ from spatial_graph._rtree._naming import module_name
58
+
59
+ pyx_dir = ROOT / "build" / "prebuilt-pyx"
60
+ pyx_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ # the template `#include`s these at compile time, so they never appear in
63
+ # `sources`; without `depends` an edit to the C core would leave every
64
+ # prebuilt module stale (the JIT path declares the same set to witty)
65
+ rtree_dir = SRC / "spatial_graph" / "_rtree"
66
+ depends = [str(rtree_dir / "src" / f) for f in ("rtree.c", "rtree.h", "config.h")]
67
+
68
+ extensions = []
69
+ for spec in iter_specs():
70
+ name = module_name(spec.cls, spec.item_dtype, spec.coord_dtype, spec.dims)
71
+ source = build_wrapper(spec.cls, spec.item_dtype, spec.coord_dtype, spec.dims)
72
+ path = pyx_dir / f"{name}.pyx"
73
+ # only rewrite when changed, so cythonize can skip unchanged variants
74
+ if not path.is_file() or path.read_text() != source:
75
+ path.write_text(source)
76
+ extensions.append(
77
+ Extension(
78
+ f"{PREBUILT_PKG}.{name}",
79
+ sources=[str(path)],
80
+ depends=depends,
81
+ include_dirs=[str(rtree_dir)],
82
+ extra_compile_args=["/O2"] if WIN else ["-O3", "-Wno-unreachable-code"],
83
+ define_macros=[
84
+ ("Py_LIMITED_API", ABI3_HEX),
85
+ *([("RTREE_NOATOMICS", "1")] if WIN else []),
86
+ ],
87
+ py_limited_api=True,
88
+ )
89
+ )
90
+
91
+ return cythonize(
92
+ extensions,
93
+ language_level=3,
94
+ quiet=True,
95
+ nthreads=0 if WIN else os.cpu_count(),
96
+ )
97
+
98
+
99
+ def can_compile() -> bool:
100
+ """Whether this machine can build a C extension at all.
101
+
102
+ Includes `Python.h` so that a box with a C compiler but no development
103
+ headers -- the usual shape of this failure -- is caught here rather than
104
+ part-way through the real build.
105
+ """
106
+ try:
107
+ from distutils.ccompiler import new_compiler
108
+ from distutils.sysconfig import customize_compiler, get_python_inc
109
+
110
+ compiler = new_compiler()
111
+ customize_compiler(compiler) # picks up CC/CFLAGS, as build_ext does
112
+ with tempfile.TemporaryDirectory() as tmp:
113
+ probe = Path(tmp, "probe.c")
114
+ probe.write_text("#include <Python.h>\nint main(void) { return 0; }\n")
115
+ # mirror build_ext, which passes both include dirs when the
116
+ # platform-specific one differs (that is where pyconfig.h can live)
117
+ includes = [get_python_inc()]
118
+ if (plat_inc := get_python_inc(plat_specific=True)) not in includes:
119
+ includes.append(plat_inc)
120
+ compiler.compile([str(probe)], output_dir=tmp, include_dirs=includes)
121
+ except Exception:
122
+ return False
123
+ return True
124
+
125
+
126
+ # Rendering and cythonizing every variant is wasted work for commands that only
127
+ # want metadata -- without this, `build --sdist` pays for a full codegen pass.
128
+ METADATA_ONLY = {"egg_info", "dist_info", "sdist"}
129
+ NEEDS_EXTENSIONS = {
130
+ "bdist_egg",
131
+ "bdist_wheel",
132
+ "build",
133
+ "build_ext",
134
+ "build_py",
135
+ "develop",
136
+ "editable_wheel",
137
+ "install",
138
+ }
139
+
140
+
141
+ def metadata_only() -> bool:
142
+ """Whether this invocation asks for nothing that needs the extensions.
143
+
144
+ Matches on recognized command names only, so option values (`--dist-dir
145
+ /tmp/x`) are ignored, and anything unrecognized falls through to building
146
+ -- skipping wrongly would silently yield a wheel with no prebuilt modules.
147
+ """
148
+ seen = {arg for arg in sys.argv[1:] if arg in METADATA_ONLY | NEEDS_EXTENSIONS}
149
+ return bool(seen) and seen <= METADATA_ONLY
150
+
151
+
152
+ def should_prebuild() -> bool:
153
+ """Whether to compile prebuilt variants into this wheel."""
154
+ if metadata_only():
155
+ return False
156
+
157
+ _src_on_path()
158
+ from spatial_graph._rtree._naming import env_enabled
159
+
160
+ if env_enabled("SPATIAL_GRAPH_NO_PREBUILT"):
161
+ return False
162
+ if env_enabled("SPATIAL_GRAPH_REQUIRE_PREBUILT"):
163
+ return True # CI: never let a build silently degrade
164
+ if can_compile():
165
+ return True
166
+ # Installing from an sdist without a compiler must keep working: fall back to
167
+ # a pure-Python wheel that JIT-compiles on first use, as it did before
168
+ # prebuilding existed.
169
+ warnings.warn(
170
+ "No usable C compiler found; building spatial-graph without prebuilt "
171
+ "rtree modules. A C compiler will be needed the first time an RTree is "
172
+ "used.",
173
+ stacklevel=1,
174
+ )
175
+ return False
176
+
177
+
178
+ if __name__ == "__main__":
179
+ if should_prebuild():
180
+ setup(
181
+ ext_modules=prebuilt_extensions(),
182
+ options={
183
+ "bdist_wheel": {"py_limited_api": ABI3_TAG},
184
+ "build_ext": {"parallel": os.cpu_count()},
185
+ },
186
+ )
187
+ else:
188
+ setup(ext_modules=[])