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.
- spatial_graph-0.1.0/.github/workflows/ci.yml +202 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.pre-commit-config.yaml +2 -2
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/PKG-INFO +41 -24
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/README.md +27 -8
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/pyproject.toml +36 -12
- spatial_graph-0.1.0/setup.cfg +4 -0
- spatial_graph-0.1.0/setup.py +188 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/graph_base.py +10 -4
- spatial_graph-0.1.0/src/spatial_graph/_rtree/_codegen.py +72 -0
- spatial_graph-0.1.0/src/spatial_graph/_rtree/_naming.py +63 -0
- spatial_graph-0.1.0/src/spatial_graph/_rtree/_prebuilt/__init__.py +5 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/rtree.py +41 -28
- spatial_graph-0.1.0/src/spatial_graph.egg-info/PKG-INFO +209 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/SOURCES.txt +54 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/dependency_links.txt +1 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/requires.txt +14 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/scm_file_list.json +51 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/scm_version.json +8 -0
- spatial_graph-0.1.0/src/spatial_graph.egg-info/top_level.txt +1 -0
- spatial_graph-0.1.0/tests/test_prebuilt.py +97 -0
- spatial_graph-0.0.6/.github/workflows/ci.yml +0 -124
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.github/workflows/docs.yaml +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/.gitignore +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/Dockerfile +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/LICENSE +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/docs/index.md +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/examples/basic_usage.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/examples/query_nearest_vispy.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/mkdocs.yml +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/__init__.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_dtypes.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/__init__.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/graph.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/src/LICENSE.txt +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/src/graph_lite.h +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/views.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_graph/wrapper_template.pyx +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/__init__.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/line_rtree.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/point_rtree.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/ARCHITECTURE.md +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/LICENSE +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/config.h +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/rtree.c +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/src/rtree.h +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/tree_base.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_rtree/wrapper_template.pyx +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_spatial_graph.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/_util.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/src/spatial_graph/py.typed +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_attributes.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_bench.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_dtype.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_graph.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_invalid_inputs.py +0 -0
- {spatial_graph-0.0.6 → spatial_graph-0.1.0}/tests/test_rtree.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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`
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
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`
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
3
|
-
|
|
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.
|
|
6
|
-
|
|
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.
|
|
14
|
-
license =
|
|
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>=
|
|
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
|
|
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 = "
|
|
91
|
+
target-version = "py311"
|
|
68
92
|
line-length = 88
|
|
69
93
|
fix = true
|
|
70
94
|
unsafe-fixes = true
|
|
@@ -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=[])
|