From de588238a5cb0dc90b2f1ae700f44204dad4a29e Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:19:51 -0500 Subject: [PATCH 1/3] Remove the deprecated basins, crop(zones_ids=), raster= and natural_breaks legacy-order aliases for 1.0.0 (#3762) --- docs/source/reference/hydrology.rst | 1 - examples/user_guide/3_Zonal.ipynb | 2 +- xrspatial/__init__.py | 1 - xrspatial/accessor.py | 9 --- xrspatial/classify.py | 29 ---------- xrspatial/focal.py | 48 +-------------- xrspatial/hydro/__init__.py | 6 -- .../hydro/tests/test_routing_public_api.py | 4 +- xrspatial/hydro/tests/test_watershed_d8.py | 40 +++---------- xrspatial/hydro/watershed_d8.py | 12 ---- xrspatial/tests/test_accessor.py | 2 +- xrspatial/tests/test_classify.py | 10 ++-- xrspatial/tests/test_focal.py | 31 +++------- xrspatial/tests/test_zonal.py | 58 +++---------------- xrspatial/zonal.py | 24 -------- 15 files changed, 33 insertions(+), 244 deletions(-) diff --git a/docs/source/reference/hydrology.rst b/docs/source/reference/hydrology.rst index a053def8b..feb64bc5c 100644 --- a/docs/source/reference/hydrology.rst +++ b/docs/source/reference/hydrology.rst @@ -102,7 +102,6 @@ Routing variants: xrspatial.hydro.watershed_d8.watershed_d8 xrspatial.hydro.watershed_dinf.watershed_dinf xrspatial.hydro.watershed_mfd.watershed_mfd - xrspatial.hydro.watershed_d8.basins_d8 Stream Link =========== diff --git a/examples/user_guide/3_Zonal.ipynb b/examples/user_guide/3_Zonal.ipynb index 69167970e..7a3802f61 100644 --- a/examples/user_guide/3_Zonal.ipynb +++ b/examples/user_guide/3_Zonal.ipynb @@ -427,7 +427,7 @@ }, "outputs": [], "source": [ - "cropped = terrain.xrs.crop(zones_agg, zones_ids=[13, 14])\n", + "cropped = terrain.xrs.crop(zones_agg, zone_ids=[13, 14])\n", "\n", "# Show full terrain with the crop boundary, then the cropped result\n", "crop_zones = zones_agg.where(zones_agg.isin([13, 14]))\n", diff --git a/xrspatial/__init__.py b/xrspatial/__init__.py index 8dbacdd21..6cd0615e1 100644 --- a/xrspatial/__init__.py +++ b/xrspatial/__init__.py @@ -121,7 +121,6 @@ from xrspatial.visibility import line_of_sight # noqa from xrspatial.visibility import visibility_frequency # noqa from xrspatial.hydro import basin # noqa: unified wrapper -from xrspatial.hydro import basins # noqa: backward-compat alias from xrspatial.hydro import watershed # noqa: unified wrapper from xrspatial.zonal import apply as zonal_apply # noqa from xrspatial.zonal import crop # noqa diff --git a/xrspatial/accessor.py b/xrspatial/accessor.py index 55eae0310..cf874aa1e 100644 --- a/xrspatial/accessor.py +++ b/xrspatial/accessor.py @@ -1063,10 +1063,6 @@ def basin(self, **kwargs): from .hydro import basin return basin(self._obj, **kwargs) - def basins(self, **kwargs): - from .hydro import basins - return basins(self._obj, **kwargs) - def sink(self, **kwargs): from .hydro import sink return sink(self._obj, **kwargs) @@ -1863,10 +1859,6 @@ def basin(self, **kwargs): from .hydro import basin return basin(self._obj, **kwargs) - def basins(self, **kwargs): - from .hydro import basins - return basins(self._obj, **kwargs) - def sink(self, **kwargs): from .hydro import sink return sink(self._obj, **kwargs) @@ -2362,7 +2354,6 @@ def validate(self, *, raise_on_error=False): 'flow_direction': 'flow_direction_d8', 'flow_accumulation': 'flow_accumulation_d8', 'basin': 'basin_d8', - 'basins': 'basins_d8', 'watershed': 'watershed_d8', 'snap_pour_point': 'snap_pour_point_d8', 'flow_path': 'flow_path_d8', diff --git a/xrspatial/classify.py b/xrspatial/classify.py index 169eaa3c3..f9439cb9a 100644 --- a/xrspatial/classify.py +++ b/xrspatial/classify.py @@ -1,7 +1,6 @@ from __future__ import annotations import cmath -import functools import warnings from functools import partial from typing import List, Optional @@ -803,35 +802,7 @@ def _run_dask_cupy_natural_break(agg, num_sample, k): return out -def _natural_breaks_legacy_order(func): - """Bridge the pre-1.0 ``natural_breaks`` parameter order. - - The old signature was ``(agg, num_sample, name, k)``. The new order is - ``(agg, k, num_sample, name)`` to match ``quantile`` and - ``maximum_breaks``. Legacy callers always passed ``k`` as a keyword - (it was the last parameter), so a call that supplies ``k=`` together - with a second positional argument is using the old order: that - positional is the old ``num_sample``. Remap it and warn. - """ - @functools.wraps(func) - def wrapper(agg, *args, **kwargs): - if 'k' in kwargs and len(args) >= 1 and 'num_sample' not in kwargs: - warnings.warn( - "natural_breaks parameter order changed to " - "(agg, k, num_sample, name) to match the other " - "classifiers. Passing num_sample positionally is " - "deprecated; pass num_sample=... instead.", - DeprecationWarning, - stacklevel=2, - ) - kwargs['num_sample'] = args[0] - args = args[1:] - return func(agg, *args, **kwargs) - return wrapper - - @supports_dataset -@_natural_breaks_legacy_order def natural_breaks(agg: xr.DataArray, k: int = 5, num_sample: Optional[int] = 20000, diff --git a/xrspatial/focal.py b/xrspatial/focal.py index 340b3936a..5be346481 100644 --- a/xrspatial/focal.py +++ b/xrspatial/focal.py @@ -2,7 +2,6 @@ import copy import math -import warnings from functools import partial from math import isnan @@ -152,36 +151,6 @@ def _apply_per_band(band_func, agg, *args, **kwargs): return xr.concat(slices, dim=band_dim) -def _resolve_raster_alias(agg, raster, func_name): - """Support the deprecated ``raster=`` keyword for the first argument. - - The focal public functions standardised their first parameter on - ``agg`` to match the rest of the library. ``apply`` and ``hotspots`` - historically named it ``raster``; this shim keeps that keyword working - while emitting a ``DeprecationWarning``. - """ - if raster is not None: - if agg is not None: - raise TypeError( - f"{func_name}() got both 'agg' and the deprecated 'raster' " - f"argument; pass the input as 'agg' only." - ) - warnings.warn( - f"{func_name}(): the 'raster' argument is deprecated and will be " - f"removed in a future release; use 'agg' instead.", - DeprecationWarning, - # stacklevel=3 points the warning at the caller: - # caller -> public func (apply/hotspots) -> this helper. - stacklevel=3, - ) - return raster - if agg is None: - raise TypeError( - f"{func_name}() missing required argument: 'agg'" - ) - return agg - - # TODO: Make convolution more generic with numba first-class functions. @@ -626,8 +595,8 @@ def _apply_dask_cupy(data, kernel, func, boundary='nan'): return out -def apply(agg=None, kernel=None, func=None, name='focal_apply', - boundary='nan', *, raster=None): +def apply(agg, kernel=None, func=None, name='focal_apply', + boundary='nan'): """ Returns custom function applied array using a user-created window. @@ -666,9 +635,6 @@ def apply(agg=None, kernel=None, func=None, name='focal_apply', agg : xarray.DataArray of same type as `agg` 2D aggregate array of filtered values. - .. deprecated:: - The ``raster`` keyword argument is deprecated; use ``agg``. - Examples -------- Focal apply works with NumPy backed xarray DataArray @@ -753,8 +719,6 @@ def apply(agg=None, kernel=None, func=None, name='focal_apply', [2. , 2. , 2. , 1.5]]) Dimensions without coordinates: y, x """ - agg = _resolve_raster_alias(agg, raster, 'apply') - _validate_raster(agg, func_name='apply', name='agg', ndim=(2, 3)) if func is None: @@ -1735,8 +1699,7 @@ def _hotspots_cupy(raster, kernel, boundary='nan'): return out -def hotspots(agg=None, kernel=None, name='hotspots', boundary='nan', *, - raster=None): +def hotspots(agg, kernel=None, name='hotspots', boundary='nan'): """ Identify statistically significant hot spots and cold spots in an input raster using the Getis-Ord Gi* statistic. To be a @@ -1791,9 +1754,6 @@ def hotspots(agg=None, kernel=None, name='hotspots', boundary='nan', *, hotspots_agg : xarray.DataArray of same type as `agg` 2D array of hotspots with values indicating confidence level. - .. deprecated:: - The ``raster`` keyword argument is deprecated; use ``agg``. - Examples -------- .. sourcecode:: python @@ -1816,8 +1776,6 @@ def hotspots(agg=None, kernel=None, name='hotspots', boundary='nan', *, Dimensions without coordinates: dim_0, dim_1 """ - agg = _resolve_raster_alias(agg, raster, 'hotspots') - _validate_raster(agg, func_name='hotspots', name='agg', ndim=(2, 3)) if agg.ndim == 3: diff --git a/xrspatial/hydro/__init__.py b/xrspatial/hydro/__init__.py index 2912db836..93514ebf2 100644 --- a/xrspatial/hydro/__init__.py +++ b/xrspatial/hydro/__init__.py @@ -23,7 +23,6 @@ from xrspatial.hydro.stream_link_d8 import stream_link_d8 # noqa from xrspatial.hydro.stream_order_d8 import stream_order_d8 # noqa from xrspatial.hydro.twi_d8 import twi_d8 # noqa -from xrspatial.hydro.watershed_d8 import basins_d8 # noqa from xrspatial.hydro.watershed_d8 import watershed_d8 # noqa # -- concrete D-infinity implementations ----------------------------------- @@ -165,11 +164,6 @@ def __call__(self, *args, routing='d8', ordering='strahler', **kwargs): summary="Label each cell with the outlet of the basin it drains to.", d8=basin_d8, ) -basins = _RoutingDispatch( - 'basins', - summary="Deprecated alias for :func:`basin`.", - d8=basins_d8, -) sink = _RoutingDispatch( 'sink', summary="Label depression cells that have no downstream neighbor.", diff --git a/xrspatial/hydro/tests/test_routing_public_api.py b/xrspatial/hydro/tests/test_routing_public_api.py index de3962ba7..9c69e0477 100644 --- a/xrspatial/hydro/tests/test_routing_public_api.py +++ b/xrspatial/hydro/tests/test_routing_public_api.py @@ -17,7 +17,7 @@ WRAPPERS = [ 'flow_direction', 'flow_accumulation', 'flow_length', 'flow_path', 'watershed', 'hand', 'stream_link', 'stream_order', - 'basin', 'basins', 'sink', 'snap_pour_point', 'fill', 'twi', + 'basin', 'sink', 'snap_pour_point', 'fill', 'twi', ] # Representative suffixed implementations across every family. @@ -26,7 +26,7 @@ 'flow_accumulation_d8', 'flow_accumulation_dinf', 'flow_accumulation_mfd', 'flow_length_d8', 'flow_path_mfd', 'watershed_dinf', 'hand_mfd', 'stream_link_d8', 'stream_order_dinf', - 'basin_d8', 'basins_d8', 'sink_d8', 'snap_pour_point_d8', 'fill_d8', + 'basin_d8', 'sink_d8', 'snap_pour_point_d8', 'fill_d8', 'twi_d8', ] diff --git a/xrspatial/hydro/tests/test_watershed_d8.py b/xrspatial/hydro/tests/test_watershed_d8.py index 8e653a963..16d29414f 100644 --- a/xrspatial/hydro/tests/test_watershed_d8.py +++ b/xrspatial/hydro/tests/test_watershed_d8.py @@ -2,7 +2,7 @@ import pytest import xarray as xr -from xrspatial.hydro import watershed, basins +from xrspatial.hydro import watershed from xrspatial.tests.general_checks import ( create_test_raster, cuda_and_cupy_available, @@ -165,39 +165,15 @@ def test_multiple_paths_to_same_pour_point(): # ==================================================================== -# Backward-compat: basins() still works via wrapper +# The ``basins`` / ``basins_d8`` aliases were removed in 1.0.0 # ==================================================================== -def test_basins_backward_compat(): - """basins() wrapper delegates to basin() correctly.""" - from xrspatial.hydro import basin - - flow_dir = np.array([ - [2.0, 4.0, 8.0], - [1.0, 0.0, 16.0], - [128.0, 64.0, 32.0], - ], dtype=np.float64) - fd_da = create_test_raster(flow_dir) - old_result = basins(fd_da) - new_result = basin(fd_da) - np.testing.assert_allclose(old_result.data, new_result.data, equal_nan=True) - - -def test_basins_d8_emits_deprecation_warning(): - """basins_d8 wrapper warns and still returns the basin result.""" - from xrspatial.hydro.basin_d8 import basin_d8 - from xrspatial.hydro.watershed_d8 import basins_d8 - - flow_dir = np.array([ - [2.0, 4.0, 8.0], - [1.0, 0.0, 16.0], - [128.0, 64.0, 32.0], - ], dtype=np.float64) - fd_da = create_test_raster(flow_dir) - with pytest.warns(DeprecationWarning, match="basins_d8 is deprecated"): - old_result = basins_d8(fd_da) - new_result = basin_d8(fd_da) - np.testing.assert_allclose(old_result.data, new_result.data, equal_nan=True) +def test_basins_alias_removed(): + import xrspatial + from xrspatial.hydro import watershed_d8 + assert not hasattr(xrspatial, 'basins') + assert not hasattr(xrspatial.hydro, 'basins') + assert not hasattr(watershed_d8, 'basins_d8') # ==================================================================== diff --git a/xrspatial/hydro/watershed_d8.py b/xrspatial/hydro/watershed_d8.py index 3a051791b..1af16f290 100644 --- a/xrspatial/hydro/watershed_d8.py +++ b/xrspatial/hydro/watershed_d8.py @@ -1070,15 +1070,3 @@ def watershed_d8(flow_dir: xr.DataArray, coords=flow_dir.coords, dims=flow_dir.dims, attrs=flow_dir.attrs) - - -def basins_d8(flow_dir, name='basins'): - """Backward-compatible wrapper; use :func:`basin` instead.""" - import warnings - warnings.warn( - "basins_d8 is deprecated; use basin (basin_d8) instead.", - DeprecationWarning, - stacklevel=2, - ) - from .basin_d8 import basin_d8 - return basin_d8(flow_dir, name=name) diff --git a/xrspatial/tests/test_accessor.py b/xrspatial/tests/test_accessor.py index e2f73bd0a..9599a768d 100644 --- a/xrspatial/tests/test_accessor.py +++ b/xrspatial/tests/test_accessor.py @@ -579,7 +579,7 @@ def test_hydro_accessor_matches_direct(method_name, elevation): _ALL_HYDRO_METHODS = [ 'flow_direction', 'flow_direction_dinf', 'flow_direction_mfd', - 'flow_accumulation', 'flow_accumulation_mfd', 'watershed', 'basin', 'basins', + 'flow_accumulation', 'flow_accumulation_mfd', 'watershed', 'basin', 'sink', 'fill', 'stream_order', 'stream_link', 'snap_pour_point', 'flow_path', 'flow_length', 'twi', 'hand', ] diff --git a/xrspatial/tests/test_classify.py b/xrspatial/tests/test_classify.py index 771e005f8..784d7a093 100644 --- a/xrspatial/tests/test_classify.py +++ b/xrspatial/tests/test_classify.py @@ -1109,13 +1109,11 @@ def test_natural_breaks_positional_k_matches_siblings(): np.testing.assert_array_equal(positional.data, keyword.data) -def test_natural_breaks_legacy_positional_num_sample_warns(): - """Legacy (agg, num_sample, k=...) order warns and still maps correctly.""" +def test_natural_breaks_legacy_positional_num_sample_removed(): + """The pre-1.0 (agg, num_sample, k=...) remap is gone (#3762).""" agg = input_data() - with pytest.warns(DeprecationWarning): - legacy = natural_breaks(agg, 20000, k=3) - new = natural_breaks(agg, k=3, num_sample=20000) - np.testing.assert_array_equal(legacy.data, new.data) + with pytest.raises(TypeError, match="k"): + natural_breaks(agg, 20000, k=3) # =================================================================== diff --git a/xrspatial/tests/test_focal.py b/xrspatial/tests/test_focal.py index 3670387b6..004898f13 100644 --- a/xrspatial/tests/test_focal.py +++ b/xrspatial/tests/test_focal.py @@ -2333,32 +2333,17 @@ def test_focal_stats_dask_advertised_dtype_matches_computed_3217(backend, in_dty _api_kernel = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.float64) -def test_apply_raster_kwarg_deprecated(): +def test_apply_raster_kwarg_removed(): + """The 0.x raster= spelling is gone in 1.0.0 (#3762); agg is the input.""" agg = xr.DataArray(data_random) - with pytest.warns(DeprecationWarning, match="raster"): - deprecated = apply(raster=agg, kernel=_api_kernel) - current = apply(agg, _api_kernel) - np.testing.assert_array_equal(deprecated.data, current.data) + with pytest.raises(TypeError, match="raster"): + apply(raster=agg, kernel=_api_kernel) -def test_apply_raster_and_agg_conflict(): - agg = xr.DataArray(data_random) - with pytest.raises(TypeError): - apply(agg, _api_kernel, raster=agg) - - -def test_hotspots_raster_kwarg_deprecated(): - agg = xr.DataArray(data_random * 100) - with pytest.warns(DeprecationWarning, match="raster"): - deprecated = hotspots(raster=agg, kernel=_api_kernel) - current = hotspots(agg, _api_kernel) - np.testing.assert_array_equal(deprecated.data, current.data) - - -def test_hotspots_raster_and_agg_conflict(): +def test_hotspots_raster_kwarg_removed(): agg = xr.DataArray(data_random * 100) - with pytest.raises(TypeError): - hotspots(agg, _api_kernel, raster=agg) + with pytest.raises(TypeError, match="raster"): + hotspots(raster=agg, kernel=_api_kernel) def test_focal_stats_name(): @@ -2437,8 +2422,6 @@ def test_hotspots_name_gpu(): import cupy agg = xr.DataArray(cupy.asarray(data_random * 100)) assert hotspots(agg, _api_kernel).name == 'hotspots' - with pytest.warns(DeprecationWarning, match="raster"): - hotspots(raster=agg, kernel=_api_kernel) # --------------------------------------------------------------------------- diff --git a/xrspatial/tests/test_zonal.py b/xrspatial/tests/test_zonal.py index 5157816b2..b62a5e3ab 100644 --- a/xrspatial/tests/test_zonal.py +++ b/xrspatial/tests/test_zonal.py @@ -2248,54 +2248,10 @@ def test_crop_nothing_to_crop(): # --------------------------------------------------------------------------- -# Regression tests for #2521: crop() should accept the canonical zone_ids -# kwarg (matching stats() and crosstab()), with zones_ids kept as a -# deprecated alias. +# Regression tests for #2521: crop() takes the canonical zone_ids kwarg +# (matching stats() and crosstab()). The zones_ids alias was removed in 1.0.0. # --------------------------------------------------------------------------- -def test_crop_zone_ids_canonical_matches_zones_ids_legacy(): - """crop(..., zone_ids=...) produces the same result as the legacy alias.""" - import warnings - - arr = np.array([[0, 4, 0, 3], - [0, 4, 4, 3], - [0, 1, 1, 3], - [0, 1, 1, 3], - [0, 0, 0, 0]], dtype=np.int64) - raster = create_test_arr(arr) - - new = crop(raster, raster, zone_ids=(1, 3)) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - legacy = crop(raster, raster, zones_ids=(1, 3)) - - assert new.shape == legacy.shape - assert (new.data == legacy.data).all() - - -def test_crop_zones_ids_emits_deprecation_warning(): - """Passing zones_ids must emit a DeprecationWarning.""" - import warnings - - arr = np.array([[0, 4, 0, 3], - [0, 4, 4, 3], - [0, 1, 1, 3], - [0, 1, 1, 3], - [0, 0, 0, 0]], dtype=np.int64) - raster = create_test_arr(arr) - - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - crop(raster, raster, zones_ids=(1, 3)) - - dep = [w for w in caught if issubclass(w.category, DeprecationWarning) - and "zones_ids" in str(w.message)] - assert len(dep) >= 1, ( - f"Expected a DeprecationWarning mentioning zones_ids, got: " - f"{[str(w.message) for w in caught]}" - ) - - def test_crop_zone_ids_does_not_warn(): """Passing zone_ids (canonical) must not emit a DeprecationWarning.""" import warnings @@ -2319,8 +2275,8 @@ def test_crop_zone_ids_does_not_warn(): ) -def test_crop_both_aliases_raises(): - """Passing both zone_ids and zones_ids must raise TypeError.""" +def test_crop_zones_ids_removed(): + """The 0.x zones_ids alias is gone in 1.0.0 (#3762).""" arr = np.array([[0, 4, 0, 3], [0, 4, 4, 3], [0, 1, 1, 3], @@ -2328,12 +2284,12 @@ def test_crop_both_aliases_raises(): [0, 0, 0, 0]], dtype=np.int64) raster = create_test_arr(arr) - with pytest.raises(TypeError, match="zone_ids.*zones_ids|zones_ids.*zone_ids"): - crop(raster, raster, zone_ids=(1,), zones_ids=(3,)) + with pytest.raises(TypeError, match="zones_ids"): + crop(raster, raster, zones_ids=(1, 3)) def test_crop_missing_zone_ids_raises(): - """crop() with neither zone_ids nor zones_ids must raise TypeError.""" + """crop() without zone_ids must raise TypeError.""" arr = np.array([[0, 4, 0, 3], [0, 4, 4, 3]], dtype=np.int64) raster = create_test_arr(arr) diff --git a/xrspatial/zonal.py b/xrspatial/zonal.py index 6e92f96be..3eb44efb1 100644 --- a/xrspatial/zonal.py +++ b/xrspatial/zonal.py @@ -2871,7 +2871,6 @@ def crop( name: str = "crop", column: Optional[str] = None, rasterize_kw: Optional[dict] = None, - zones_ids: Optional[Union[list, tuple]] = None, ): """ Crop scans from edges and eliminates rows / cols until one of the @@ -2901,10 +2900,6 @@ def crop( Extra keyword arguments forwarded to ``rasterize()`` when *zones* is vector input. - zones_ids : list or tuple, optional - Deprecated alias for ``zone_ids``. Will emit a - ``DeprecationWarning`` and be removed in a future release. - Returns ------- crop_agg : xarray.DataArray @@ -3009,25 +3004,6 @@ def crop( 'Max Elevation': '4000', } """ - # Backwards-compatible alias: stats() and crosstab() use `zone_ids`, - # crop() historically used `zones_ids` (extra 's'). Accept both, - # emit a DeprecationWarning on the old name, raise if both are passed. - if zones_ids is not None: - import warnings - if zone_ids is not None: - raise TypeError( - "crop() received both `zone_ids` and `zones_ids`; pass " - "only `zone_ids` (the canonical name)." - ) - warnings.warn( - "crop(zones_ids=...) is deprecated and will be removed in a " - "future release; use `zone_ids=...` to match stats() and " - "crosstab().", - DeprecationWarning, - stacklevel=2, - ) - zone_ids = zones_ids - if zone_ids is None: raise TypeError( "crop() missing required argument `zone_ids` (list or tuple " From 58a845ee279754d2dd8a7c1dfd66f589d7267104 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:20:45 -0500 Subject: [PATCH 2/3] Rename the accessor crop() parameter to zone_ids and drop the last basins mention (#3762) --- xrspatial/accessor.py | 4 ++-- xrspatial/hydro/watershed_d8.py | 2 +- xrspatial/tests/test_accessor.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/xrspatial/accessor.py b/xrspatial/accessor.py index cf874aa1e..d2e20aaee 100644 --- a/xrspatial/accessor.py +++ b/xrspatial/accessor.py @@ -1301,9 +1301,9 @@ def zonal_hypsometric_integral(self, zones, **kwargs): from .zonal import hypsometric_integral return hypsometric_integral(zones, self._obj, **kwargs) - def crop(self, zones, zones_ids, **kwargs): + def crop(self, zones, zone_ids, **kwargs): from .zonal import crop - return crop(zones, self._obj, zones_ids, **kwargs) + return crop(zones, self._obj, zone_ids, **kwargs) def clip_polygon(self, geometry, **kwargs): from .polygon_clip import clip_polygon diff --git a/xrspatial/hydro/watershed_d8.py b/xrspatial/hydro/watershed_d8.py index 1af16f290..2f33624bb 100644 --- a/xrspatial/hydro/watershed_d8.py +++ b/xrspatial/hydro/watershed_d8.py @@ -3,7 +3,7 @@ Two complementary functions: - ``watershed(flow_dir, pour_points)`` — labels each cell with the pour point it drains to; cells not reaching any pour point → NaN. -- ``basins(flow_dir)`` — automatically identifies all outlets (pits + +- ``basin(flow_dir)`` — automatically identifies all outlets (pits + edge-exit cells) and labels every valid cell; no pour points needed. Both use **downstream tracing with path compression** on CPU — follow diff --git a/xrspatial/tests/test_accessor.py b/xrspatial/tests/test_accessor.py index 9599a768d..bc3338978 100644 --- a/xrspatial/tests/test_accessor.py +++ b/xrspatial/tests/test_accessor.py @@ -865,3 +865,20 @@ def test_coregister_raster_preserves_dask_backend(): out = grid.xrs.coregister(src) assert isinstance(out.data, da.Array) assert out.shape == grid.shape + + +def test_accessor_crop_uses_zone_ids_keyword(): + """The accessor's crop() takes zone_ids like the function; zones_ids is gone (#3762).""" + import pytest + from xrspatial.zonal import crop + + arr = np.array([[0, 4, 0, 3], + [0, 4, 4, 3], + [0, 1, 1, 3], + [0, 0, 0, 0]], dtype=np.int64) + raster = xr.DataArray(arr, dims=('y', 'x')) + via_accessor = raster.xrs.crop(raster, zone_ids=(1, 3)) + direct = crop(raster, raster, zone_ids=(1, 3)) + xr.testing.assert_identical(via_accessor, direct) + with pytest.raises(TypeError, match="zone_ids"): + raster.xrs.crop(raster, zones_ids=(1, 3)) From 078a60c79191b289ff4271ea88ac63117caebab6 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:22:04 -0500 Subject: [PATCH 3/3] Address review nit: drop the stale zones_ids filter in the crop no-warning test (#3762) --- xrspatial/tests/test_zonal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xrspatial/tests/test_zonal.py b/xrspatial/tests/test_zonal.py index b62a5e3ab..9c839f6c3 100644 --- a/xrspatial/tests/test_zonal.py +++ b/xrspatial/tests/test_zonal.py @@ -2267,8 +2267,7 @@ def test_crop_zone_ids_does_not_warn(): warnings.simplefilter("always") crop(raster, raster, zone_ids=(1, 3)) - dep = [w for w in caught if issubclass(w.category, DeprecationWarning) - and "zones_ids" in str(w.message)] + dep = [w for w in caught if issubclass(w.category, DeprecationWarning)] assert dep == [], ( f"Expected no DeprecationWarning for zone_ids, got: " f"{[str(w.message) for w in dep]}"