Testing and Validating Spatial Wheels
A geospatial wheel that compiles and repairs cleanly can still be broken in ways only a runtime check reveals: a missing datum database, a platform tag PyPI will reject, or a bundled library that resolves to the host on the build box and to nothing on a user’s machine. This guide sits under the Modern Python Build Tooling & Wheel Configuration reference and defines the validation gate every spatial wheel should pass before publication — static tag inspection, clean-container smoke tests, and functional transforms that exercise GDAL and PROJ end to end. It targets auditwheel 6.x, delocate 0.11+, twine 5.x, and wheels built to the abi3 contract from C-API vs CPython ABI compatibility.
Prerequisites & Environment
- The repaired wheels under test (from
wheelhouse/ordist/), not the source tree. auditwheel6.x (Linux) /delocate0.11+ (macOS) for static inspection.- Docker with a clean runtime image (
python:3.12-slim) that has no geospatial libraries — the only honest test environment. twine5.x for metadata validation.
# The wheels to validate — list them first
ls dist/*.whl
Core Configuration
The four gates are ordered from cheapest to most thorough, and each catches a class the previous cannot:
| Gate | Command | Catches |
|---|---|---|
| Metadata | twine check dist/* |
Broken long-description, missing fields PyPI rejects |
| Platform tag | auditwheel show wheel.whl |
Unrepaired linux_x86_64 tag, host-leaking references |
| Load | clean-container import |
Missing/mislinked bundled libgdal/libproj |
| Function | transform in clean container | Missing proj.db, wrong datum data, silent API breaks |
Running only the first two is the common mistake: a wheel can have a perfect manylinux_2_28 tag and still raise DataDirError at the first transform because the data files were never packaged. The load and function gates are covered in depth by smoke-testing GDAL wheels in a clean container, and the tag gate by verifying wheel tags with auditwheel show.
Step-by-Step Implementation
-
Metadata gate:
python -m twine check dist/*.whl # PASSED for each -
Tag gate:
auditwheel show dist/*manylinux*.whl | grep -iE 'platform|following' # platform tag must be manylinux_2_28_x86_64; external refs must be only base libc -
Load gate in a clean container:
docker run --rm -v "$PWD/dist:/d" python:3.12-slim \ bash -c "pip install /d/*manylinux*.whl && python -c 'from osgeo import gdal'" -
Function gate — a transform that needs the datum database:
docker run --rm -v "$PWD/dist:/d" python:3.12-slim bash -c " pip install /d/*manylinux*.whl && python -c \"import pyproj; print(pyproj.Transformer.from_crs(4326,3857).transform(52,5))\""
Verification
The gate passes only when all four succeed on the same wheel:
# One-shot gate: any failure exits non-zero
set -e
python -m twine check dist/*manylinux*.whl
auditwheel show dist/*manylinux*.whl | grep -q manylinux_2_28
docker run --rm -v "$PWD/dist:/d" python:3.12-slim bash -c \
"pip install -q /d/*manylinux*.whl && python -c 'import pyproj; pyproj.Transformer.from_crs(4326,3857).transform(52,5)'"
echo "ALL GATES PASSED"
ALL GATES PASSED means the wheel is publishable. A non-zero exit before it names the failing gate, which maps directly to a fix in the child pages.
Optimization & Edge Cases
- Test the oldest supported interpreter. An abi3 wheel floored at 3.9 should be smoke-tested on 3.9 and the newest 3.x, since the Stable ABI is the contract being validated.
- musllinux needs an Alpine image. A
musllinux_1_2wheel cannot be validated in a glibcslimimage; usepython:3.12-alpine. - Wire the gate into CI as a job. Running these only locally means they run rarely; add them as a
teststage after the build matrix.
Troubleshooting
twine check warns about the description. A malformed README long_description blocks upload. Fix the metadata in pyproject.toml and rebuild.
Tag says linux_x86_64. The wheel was never repaired. Run auditwheel repair before testing — the tag gate exists precisely to catch this.
Import passes but transform raises DataDirError. The library loaded but the data did not ship. Bundle it per bundling proj.db and datum grids in a wheel.
Related
- Smoke-testing GDAL wheels in a clean container — the load and function gates in detail, with a reusable test image.
- Verifying wheel tags with auditwheel show — reading the platform tag and external-reference report.
- Bundling proj.db and datum grids in a wheel — the fix when the function gate fails on data.
Further Reading
auditwheelandtwinedocumentation for the authoritative check semantics.