Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e .[tests] \
numba==0.59.0 scipy==1.11.2 xarray==2025.11.0 \
numpy==1.26.0 pandas==2.2.0 urllib3==2.7.0 zstandard==0.22.0
numba==0.60.0 scipy==1.13.0 xarray==2025.11.0 \
numpy==2.0.0 pandas==2.2.2 urllib3==2.7.0 zstandard==0.22.0
- name: Run pytest (fast lane)
run: pytest -m "not slow"

Expand Down
28 changes: 18 additions & 10 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ include_package_data = True
# .github/workflows/test.yml, which installs exactly these versions and
# runs the suite against them. Issue #3701 has the full derivation.
#
# numba, scipy oldest release shipping a CPython 3.12 wheel, which
# is the floor python_requires allows anyway
# numpy >=1.26 first cp312 wheel, and xarray's own numpy floor
# numpy >=2.0 NumPy 1.x is not supported. xrspatial/__init__.py
# re-checks this at import and raises ImportError,
# because pyarrow (tests extra) declares no numpy
# requirement pip could act on and numba's own check
# fires deep inside a submodule import.
# numba >=0.60 first release that accepts numpy 2 (0.59 declares
# numpy<1.27)
# scipy >=1.13 first release built against numpy 2 (1.11 and
# 1.12 declare numpy<1.28)
# numpy <2.5 mirrors numba: 0.64-0.66 declare numpy<2.5 and numba
# re-checks it at import in _ensure_critical_deps.
# Without this, pip installs a numpy that makes
Expand All @@ -35,17 +41,19 @@ include_package_data = True
# ops, so morph_gradient / morph_white_tophat /
# morph_black_tophat and the mcda and corridor
# metadata tests lose res and crs
# pandas >=2.2 required by xarray >=2025.11; pandas is a direct
# import here (zonal, dasymetric, focal) and part of
# the public API -- zonal_stats returns a DataFrame
# pandas >=2.2.2 first 2.2.x without the numpy<2 cap (2.2.0 and
# 2.2.1 declare it); xarray >=2025.11 needs >=2.2
# anyway. pandas is a direct import here (zonal,
# dasymetric, focal) and part of the public API --
# zonal_stats returns a DataFrame
# urllib3 >=2.7 first release with no open advisory against the
# streaming read path used by the COG-over-HTTP source
install_requires =
numba >=0.59
scipy >=1.11.2
numba >=0.60
scipy >=1.13
xarray >=2025.11
numpy >=1.26,<2.5
pandas >=2.2
numpy >=2.0,<2.5
pandas >=2.2.2
urllib3 >=2.7
packages = find:
python_requires = >=3.12
Expand Down
1 change: 1 addition & 0 deletions xrspatial/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from xrspatial._numpy_floor import _check_numpy_version # noqa: F401
from xrspatial.aspect import aspect # noqa
from xrspatial.aspect import eastness # noqa
from xrspatial.aspect import northness # noqa
Expand Down
25 changes: 25 additions & 0 deletions xrspatial/_numpy_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Import-time NumPy floor. Imported first by xrspatial/__init__.py.

numba and pyarrow both reject numpy 1.x in their own ways (numba at
import, pyarrow 26 at import with no metadata pin that pip could act
on), which surfaces deep inside a submodule import as an unrelated
error. Checking here, before any submodule loads, turns that into one
plain sentence that names the version found.
"""
import numpy as _np


def _check_numpy_version(version):
"""Raise ImportError if ``version`` is a NumPy 1.x release string."""
try:
major = int(version.split(".")[0])
except ValueError:
return
if major < 2:
raise ImportError(
f"xarray-spatial requires NumPy 2.0 or newer, found {version}. "
"Upgrade with: pip install 'numpy>=2'"
)


_check_numpy_version(_np.__version__)
12 changes: 12 additions & 0 deletions xrspatial/tests/test_dependency_metadata_3701.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ def test_every_required_dependency_has_a_lower_bound():
f"{name} is declared without a lower bound"


def test_numpy_floor_is_two():
"""NumPy 1.x is not supported; the floor and the import-time check agree.

xrspatial/__init__.py raises ImportError on numpy < 2. The metadata
has to say the same thing or pip will happily install a combination
the package then refuses to import.
"""
spec = _install_requires()["numpy"]
assert not spec.contains("1.26.4"), "setup.cfg still admits numpy 1.x"
assert spec.contains("2.0.0")


def test_numpy_has_an_upper_bound():
"""numba caps numpy and re-checks the cap at import time.

Expand Down
30 changes: 30 additions & 0 deletions xrspatial/tests/test_numpy_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""The import-time NumPy floor check in xrspatial/__init__.py.

NumPy 1.x is not supported. Nothing in the dependency metadata stops a
user from ending up with numpy 1.26 next to a numba or pyarrow build
that rejects it (pyarrow 26 declares no numpy requirement at all and
raises at import), so the package checks once, up front, and says
plainly what it found.
"""
import pytest

from xrspatial import _check_numpy_version


@pytest.mark.parametrize("version", ["1.26.0", "1.26.4", "1.26.0.dev0+abc", "1.99.9"])
def test_numpy_1x_is_rejected(version):
with pytest.raises(ImportError) as excinfo:
_check_numpy_version(version)
msg = str(excinfo.value)
assert "NumPy 2.0 or newer" in msg
assert version in msg


@pytest.mark.parametrize("version", ["2.0.0", "2.0.2", "2.5.4", "2.6.0.dev0+git", "3.0.0"])
def test_numpy_2_and_later_pass(version):
_check_numpy_version(version)


def test_installed_numpy_passes_the_check():
import numpy
_check_numpy_version(numpy.__version__)
Loading