From c82e837fd201266bfe2f392af476197fce02e8dd Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Mon, 12 Oct 2026 00:18:55 -0400 Subject: [PATCH] Drop NumPy 1.x and refuse it at import time install_requires now says numpy >=2.0, and xrspatial/__init__.py imports a one-function module first that raises ImportError: xarray-spatial requires NumPy 2.0 or newer, found 1.26.4 before any submodule loads. The min-deps job went red on 2026-10-10 when pyarrow 26.0.0 shipped: it needs numpy 2 but declares no numpy requirement, so pip installed it beside the pinned numpy 1.26.0 and the failure surfaced as an AttributeError in zonal three frames from the cause (#3751). numba's own numpy check has the same shape. The other floors move to their first numpy-2-capable releases so the min-deps job can still install the set it declares: numba 0.60, scipy 1.13, pandas 2.2.2. The setup.cfg derivation comments and the min-deps pins follow. Closes #3751. --- .github/workflows/test.yml | 4 +-- setup.cfg | 28 ++++++++++------- xrspatial/__init__.py | 1 + xrspatial/_numpy_floor.py | 25 ++++++++++++++++ .../tests/test_dependency_metadata_3701.py | 12 ++++++++ xrspatial/tests/test_numpy_floor.py | 30 +++++++++++++++++++ 6 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 xrspatial/_numpy_floor.py create mode 100644 xrspatial/tests/test_numpy_floor.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5b9eca1d..d0d0bf06d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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" diff --git a/setup.cfg b/setup.cfg index ca19a48fb..56cecc796 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 @@ -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 diff --git a/xrspatial/__init__.py b/xrspatial/__init__.py index 6e3e7257c..8dbacdd21 100644 --- a/xrspatial/__init__.py +++ b/xrspatial/__init__.py @@ -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 diff --git a/xrspatial/_numpy_floor.py b/xrspatial/_numpy_floor.py new file mode 100644 index 000000000..9bc11b2ea --- /dev/null +++ b/xrspatial/_numpy_floor.py @@ -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__) diff --git a/xrspatial/tests/test_dependency_metadata_3701.py b/xrspatial/tests/test_dependency_metadata_3701.py index 1ff2162f0..a7ccf8841 100644 --- a/xrspatial/tests/test_dependency_metadata_3701.py +++ b/xrspatial/tests/test_dependency_metadata_3701.py @@ -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. diff --git a/xrspatial/tests/test_numpy_floor.py b/xrspatial/tests/test_numpy_floor.py new file mode 100644 index 000000000..98dc9252b --- /dev/null +++ b/xrspatial/tests/test_numpy_floor.py @@ -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__)