From 3e5f11ac90dd6592775e98da8f5cc6ab6219a75f Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:22:12 -0500 Subject: [PATCH 1/4] Remove deprecated GeoTIFF aliases for 1.0.0 (#3759) Drops plot_geotiff and the mask_nodata / name / mask_and_scale aliases on open_geotiff, the gpu= string alias on _read_geotiff_gpu, and the vrt_path / crs_wkt aliases on _build_vrt. Passing an old keyword now raises the normal TypeError. Deletes the sentinels that only served the shims and converts the alias tests to removal tests. --- docs/source/reference/geotiff.rst | 1 - xrspatial/geotiff/__init__.py | 103 +------------- xrspatial/geotiff/_backends/gpu.py | 37 +---- xrspatial/geotiff/_errors.py | 2 +- xrspatial/geotiff/_runtime.py | 44 +----- xrspatial/geotiff/_validation.py | 8 +- xrspatial/geotiff/_writers/vrt.py | 90 +----------- xrspatial/geotiff/_xarray_backend.py | 4 +- xrspatial/geotiff/tests/gpu/test_codec.py | 4 +- .../tests/gpu/test_kernels_and_kwargs.py | 131 +++--------------- .../tests/integration/test_dask_pipeline.py | 6 +- .../geotiff/tests/parity/test_finalization.py | 6 +- .../tests/parity/test_signature_contract.py | 19 +-- .../tests/read/test_rioxarray_compat_2961.py | 50 ++----- .../tests/release_gates/test_features.py | 29 +--- .../release_gates/test_stable_features.py | 2 +- xrspatial/geotiff/tests/unit/test_metadata.py | 9 -- xrspatial/geotiff/tests/write/test_basic.py | 121 +++------------- xrspatial/geotiff/tests/write/test_cog.py | 2 +- xrspatial/geotiff/tests/write/test_nodata.py | 28 ++-- 20 files changed, 104 insertions(+), 592 deletions(-) diff --git a/docs/source/reference/geotiff.rst b/docs/source/reference/geotiff.rst index 14660bbef..54e429a63 100644 --- a/docs/source/reference/geotiff.rst +++ b/docs/source/reference/geotiff.rst @@ -186,7 +186,6 @@ this section is the brief. sentinel passes through and downstream callers can branch on the exact value; pass ``masked=True`` to get NaN-masked output. ``xrspatial/geotiff/tests/write/test_nodata.py`` pins this split. - (``mask_nodata`` is a deprecated alias of ``masked``.) * NaN nodata. A file that declares ``nodata=NaN`` is read with NaN in both ``attrs['nodata']`` and pixel data (NaN propagates either way). * ``attrs['masked_nodata']``. Every read sets a boolean lifecycle diff --git a/xrspatial/geotiff/__init__.py b/xrspatial/geotiff/__init__.py index c3ad25a19..32553291d 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -79,10 +79,8 @@ from ._reader import (_MAX_CLOUD_BYTES_SENTINEL, CloudSizeLimitError, PixelSafetyLimitError, UnsafeURLError) from ._reader import read_to_array as _read_to_array -from ._runtime import (_CRS_WKT_DEPRECATED_SENTINEL, _GPU_DEPRECATED_SENTINEL, # noqa: F401 - _MASK_AND_SCALE_DEPRECATED_SENTINEL, _MASK_NODATA_DEPRECATED_SENTINEL, - _MISSING_SOURCES_SENTINEL, _NAME_DEPRECATED_SENTINEL, - _ON_GPU_FAILURE_SENTINEL, GeoTIFFFallbackWarning, _geotiff_strict_mode, +from ._runtime import (_MISSING_SOURCES_SENTINEL, _ON_GPU_FAILURE_SENTINEL, # noqa: F401 + GeoTIFFFallbackWarning, _geotiff_strict_mode, _gpu_fallback_warning_message) from ._validation import (_validate_3d_writer_dims, _validate_chunks_arg, # noqa: F401 _validate_tile_size_arg) @@ -103,9 +101,7 @@ # callers can import ``xrspatial.geotiff._build_vrt``. Not public API. from ._writers.vrt import _build_vrt # noqa: F401 -# All names below are part of the supported public API. ``plot_geotiff`` -# is intentionally omitted: it is deprecated in favour of ``da.xrs.plot()`` -# and emits a ``DeprecationWarning`` when called. +# All names below are part of the supported public API. __all__ = [ 'CloudSizeLimitError', 'ConflictingCRSError', @@ -499,7 +495,6 @@ def open_geotiff(source: str | BinaryIO, *, overview_level: int | None = None, band: int | None = None, default_name: str | None = None, - name: str | None = _NAME_DEPRECATED_SENTINEL, # type: ignore[assignment] chunks: int | tuple | None = None, gpu: bool = False, max_pixels: int | None = None, @@ -515,10 +510,7 @@ def open_geotiff(source: str | BinaryIO, *, allow_internal_only_jpeg: bool = False, band_nodata: str | None = None, masked: bool = False, - mask_nodata: bool = _MASK_NODATA_DEPRECATED_SENTINEL, # type: ignore[assignment] unpack: bool = False, - mask_and_scale: bool = ( - _MASK_AND_SCALE_DEPRECATED_SENTINEL), # type: ignore[assignment] parse_coordinates: bool = True, lock: object | None = None, cache: bool = True, @@ -560,7 +552,7 @@ def open_geotiff(source: str | BinaryIO, *, ``lock``, ``cache``) so callers can move between the two with minimal edits. ``masked`` defaults to ``False`` (no sentinel-to-NaN promotion), matching ``open_rasterio``. The scale/offset option is named ``unpack`` - here; rioxarray's ``mask_and_scale`` is kept as a deprecated alias. + here (rioxarray calls it ``mask_and_scale``). Automatically dispatches to the best backend: - ``gpu=True``: GPU-accelerated read via nvCOMP (returns CuPy) @@ -621,10 +613,6 @@ def open_geotiff(source: str | BinaryIO, *, default_name : str or None [stable] Name for the DataArray. None derives it from the source file name. Matches rioxarray's ``open_rasterio`` parameter. - name : str or None - [deprecated] Deprecated alias of ``default_name``; emits a - ``DeprecationWarning``. Passing both ``default_name`` and ``name`` - raises ``TypeError``. chunks : int, tuple, or None [stable] Chunk size for Dask lazy reading. Dask reads are gated against the eager reader by the cross-backend parity @@ -702,18 +690,13 @@ def open_geotiff(source: str | BinaryIO, *, step and keep the source dtype. ``attrs['nodata']`` still carries the raw sentinel either way, so downstream code can mask explicitly. The default matches rioxarray's ``open_rasterio`` - (``masked=False``); note that earlier xrspatial releases masked - by default (``mask_nodata=True``), so a bare ``open_geotiff(path)`` - no longer promotes the sentinel to NaN. Pass ``masked=True`` and + (``masked=False``); note that 0.x releases masked by default, so + a bare ``open_geotiff(path)`` no longer promotes the sentinel to + NaN. Pass ``masked=True`` and ``dtype=`` together on a source with a maskable sentinel and the read raises ``ValueError``, because the unconditional float64 promotion (issue #2990) makes the integer cast lossy whether or not a sentinel pixel is present. - mask_nodata : bool - [deprecated] Deprecated alias of ``masked``; emits a - ``DeprecationWarning``. Passing both ``masked`` and ``mask_nodata`` - raises ``TypeError``. Note the default also changed from - ``mask_nodata=True`` to ``masked=False``. unpack : bool, default False [experimental] If True, read the source's GDAL ``SCALE`` / ``OFFSET`` metadata and return ``data * scale + offset``, masking the nodata @@ -746,10 +729,6 @@ def open_geotiff(source: str | BinaryIO, *, ``unpack=True`` applies the scale a second time. Drop those tags (and ``attrs['scale_factor']`` / ``attrs['add_offset']``) before writing if you need a clean round-trip. - mask_and_scale : bool - [deprecated] Deprecated alias of ``unpack``; emits a - ``DeprecationWarning``. Named after rioxarray's ``open_rasterio``. - Passing both ``unpack`` and ``mask_and_scale`` raises ``TypeError``. parse_coordinates : bool, default True [stable] If True (the default), build ``x`` / ``y`` coordinate arrays from the transform. If False, skip them and return a @@ -934,57 +913,6 @@ def _attach_category_attrs(da): da.attrs.update(read_pam_sidecar(source)) return da - # Resolve the rioxarray-compatible renames. ``masked`` / ``default_name`` - # are the canonical names; ``mask_nodata`` / ``name`` are deprecated - # aliases kept for back-compat. Mirrors the sentinel-based deprecation in - # ``read_geotiff_gpu`` (gpu -> on_gpu_failure): passing both the old and - # new name is ambiguous and raises, passing the old name alone warns. - if mask_nodata is not _MASK_NODATA_DEPRECATED_SENTINEL: - # ``masked`` carries a real default of False, so an explicit - # ``masked=False`` cannot be told apart from the default here; that - # one combination (``masked=False`` + ``mask_nodata=True``) does not - # raise and resolves to the ``mask_nodata`` value. This matches the - # documented stance on ``read_geotiff_gpu``'s gpu/on_gpu_failure pair. - if masked is not False: - raise TypeError( - "open_geotiff: pass either 'masked' or the deprecated " - "'mask_nodata' alias, not both.") - warnings.warn( - "open_geotiff(..., mask_nodata=...) is deprecated; use " - "masked=... instead. Note the default also changed from " - "mask_nodata=True to masked=False to match rioxarray's " - "open_rasterio: a bare open_geotiff(path) no longer promotes " - "the nodata sentinel to NaN.", - DeprecationWarning, stacklevel=2) - masked = mask_nodata - if name is not _NAME_DEPRECATED_SENTINEL: - if default_name is not None: - raise TypeError( - "open_geotiff: pass either 'default_name' or the deprecated " - "'name' alias, not both.") - warnings.warn( - "open_geotiff(..., name=...) is deprecated; use default_name=... " - "instead to match rioxarray's open_rasterio.", - DeprecationWarning, stacklevel=2) - default_name = name - if mask_and_scale is not _MASK_AND_SCALE_DEPRECATED_SENTINEL: - # ``unpack`` is the canonical name; ``mask_and_scale`` is the - # deprecated rioxarray-compatible alias. ``unpack`` carries a real - # default of False, so an explicit ``unpack=False`` cannot be told - # apart from the default; that one combination resolves to the - # ``mask_and_scale`` value, matching the ``masked`` / ``mask_nodata`` - # stance above. - if unpack is not False: - raise TypeError( - "open_geotiff: pass either 'unpack' or the deprecated " - "'mask_and_scale' alias, not both.") - warnings.warn( - "open_geotiff(..., mask_and_scale=...) is deprecated; use " - "unpack=... instead. The behaviour is unchanged: it applies the " - "source's GDAL SCALE / OFFSET and masks the nodata sentinel.", - DeprecationWarning, stacklevel=2) - unpack = mask_and_scale - # ``lock`` / ``cache`` are accepted for open_rasterio signature # compatibility. xrspatial's dask reader re-opens the source per window, # so there is no shared GDAL handle to lock and no caching backend to @@ -1254,20 +1182,3 @@ def _attach_category_attrs(da): parse_coordinates=parse_coordinates, band=band, )) - - -def plot_geotiff(da: xr.DataArray, **kwargs): - """Plot a DataArray using its embedded colormap if present. - - .. deprecated:: 0.10.0 - Use ``da.xrs.plot()`` instead. ``plot_geotiff`` is a thin wrapper - kept for backward compatibility and will be removed in a future - release. - """ - warnings.warn( - "plot_geotiff is deprecated and will be removed in a future " - "release. Use ``da.xrs.plot()`` instead.", - DeprecationWarning, - stacklevel=2, - ) - return da.xrs.plot(**kwargs) diff --git a/xrspatial/geotiff/_backends/gpu.py b/xrspatial/geotiff/_backends/gpu.py index 0196f34a6..216607337 100644 --- a/xrspatial/geotiff/_backends/gpu.py +++ b/xrspatial/geotiff/_backends/gpu.py @@ -17,8 +17,8 @@ from .._nodata import NodataLifecycle as _NL from .._reader import _MAX_CLOUD_BYTES_SENTINEL, CloudSizeLimitError, _coerce_path from .._reader import read_to_array as _read_to_array -from .._runtime import (_GPU_DEPRECATED_SENTINEL, _MISSING_SOURCES_SENTINEL, - _ON_GPU_FAILURE_SENTINEL, _geotiff_strict_mode) +from .._runtime import (_MISSING_SOURCES_SENTINEL, _ON_GPU_FAILURE_SENTINEL, + _geotiff_strict_mode) from .._validation import (_validate_chunks_arg, _validate_dispatch_kwargs, _validate_dtype_cast, _validate_predictor_sample_format) from ._gpu_helpers import (_apply_nodata_mask_gpu, _apply_orientation_geo_info, @@ -74,7 +74,6 @@ def _read_geotiff_gpu(source: str, *, band_nodata: str | None = None, mask_nodata: bool = False, mask_and_scale: bool = False, - gpu: str = _GPU_DEPRECATED_SENTINEL, ) -> xr.DataArray: """Read a GeoTIFF with GPU-accelerated decompression via Numba CUDA. @@ -174,13 +173,6 @@ def _read_geotiff_gpu(source: str, *, is unusable; transient errors inside a later ``cupy.asarray(...)`` upload (e.g. device OOM) still propagate unchanged in both modes. - gpu : str, optional - [experimental] Deprecated alias for ``on_gpu_failure``. Emits - ``DeprecationWarning`` when used. Passing both ``gpu`` and - ``on_gpu_failure`` raises ``TypeError``. The old name shipped - with values ``'auto'`` / ``'strict'`` and was easy to confuse - with the boolean ``gpu=`` kwarg on ``open_geotiff`` / - ``to_geotiff`` / ``_read_vrt``. mask_nodata : bool, default False [experimental] If True, replace the nodata sentinel with NaN (integer rasters get promoted to ``float64`` first). If False, @@ -303,28 +295,9 @@ def _read_geotiff_gpu(source: str, *, allow_experimental_codecs=allow_experimental_codecs, ) - new_passed = on_gpu_failure is not _ON_GPU_FAILURE_SENTINEL - old_passed = gpu is not _GPU_DEPRECATED_SENTINEL - if new_passed and old_passed: - # Both supplied is ambiguous regardless of which values were - # chosen (including the matching ``on_gpu_failure='auto', - # gpu='auto'`` pair). Refuse rather than silently picking one. - raise TypeError( - "_read_geotiff_gpu: pass either 'on_gpu_failure' or the " - "deprecated 'gpu' alias, not both.") - if old_passed: - warnings.warn( - "_read_geotiff_gpu(..., gpu=...) is deprecated; use " - "on_gpu_failure=... instead. The kwarg was renamed because " - "'gpu' on open_geotiff/to_geotiff/_read_vrt is a bool that " - "selects the GPU backend, while here it selects the failure " - "policy when the GPU path raises.", - DeprecationWarning, - stacklevel=2, - ) - on_gpu_failure = gpu - elif not new_passed: + if on_gpu_failure is _ON_GPU_FAILURE_SENTINEL: on_gpu_failure = 'auto' + # The failure policy is read below through the ``gpu`` local. gpu = on_gpu_failure if gpu not in ('auto', 'strict'): raise ValueError( @@ -1111,7 +1084,7 @@ def _read_geotiff_gpu_eager_via_cpu(source, *, dtype, window, overview_level, return. This helper is the eager analogue: read the whole array on CPU, push it to the device in one shot. - The user-facing ``on_gpu_failure`` / ``gpu='strict'`` kwarg is a + The user-facing ``on_gpu_failure='strict'`` kwarg is a no-op on this path. There is no GPU decode stage to be strict about: the only GPU operation is the ``cupy.asarray`` upload, and upload failures (e.g. device OOM) already propagate unconditionally. diff --git a/xrspatial/geotiff/_errors.py b/xrspatial/geotiff/_errors.py index a2e75d60c..da5578f46 100644 --- a/xrspatial/geotiff/_errors.py +++ b/xrspatial/geotiff/_errors.py @@ -219,7 +219,7 @@ class InvalidIntegerNodataError(GeoTIFFAmbiguousMetadataError): class MalformedScaleOffsetError(GeoTIFFAmbiguousMetadataError): """A present SCALE / OFFSET item cannot be parsed as a float. - Raised under ``mask_and_scale=True`` when the source's GDAL_METADATA + Raised under ``unpack=True`` when the source's GDAL_METADATA carries a ``SCALE`` or ``OFFSET`` item whose value does not parse as a float (for example ``SCALE="abc"``). ``mask_and_scale`` asks the reader to honour the scale / offset metadata, so a malformed value diff --git a/xrspatial/geotiff/_runtime.py b/xrspatial/geotiff/_runtime.py index 1bb677756..5e26e4da3 100644 --- a/xrspatial/geotiff/_runtime.py +++ b/xrspatial/geotiff/_runtime.py @@ -4,8 +4,8 @@ a single canonical binding for each sentinel and helper without threading them through ``__init__.py``. Sentinel object identity is preserved by Python's module cache: every import of this module -returns the same module instance, so ``_GPU_DEPRECATED_SENTINEL is -other._GPU_DEPRECATED_SENTINEL`` resolves correctly regardless of +returns the same module instance, so ``_ON_GPU_FAILURE_SENTINEL is +other._ON_GPU_FAILURE_SENTINEL`` resolves correctly regardless of which caller imported it. """ from __future__ import annotations @@ -14,53 +14,17 @@ # Sentinels distinguishing "user passed this kwarg explicitly" from "user # passed nothing". A plain default of None does not work because None is -# itself a value a caller could supply. ``_read_geotiff_gpu`` needs both -# sentinels so it can tell whether the deprecated ``gpu=`` and the new -# ``on_gpu_failure=`` were *each* supplied, and refuse the ambiguous -# both-supplied case regardless of which values were chosen. -# ``open_geotiff`` also uses ``_ON_GPU_FAILURE_SENTINEL`` to distinguish +# itself a value a caller could supply. +# ``open_geotiff`` uses ``_ON_GPU_FAILURE_SENTINEL`` to distinguish # "caller never set on_gpu_failure" (default sentinel: skip forwarding so # the _read_geotiff_gpu signature default applies) from "caller set # on_gpu_failure=" (forward verbatim). -_GPU_DEPRECATED_SENTINEL = object() _ON_GPU_FAILURE_SENTINEL = object() -# ``_build_vrt`` needs to distinguish "user passed crs_wkt= explicitly" -# (deprecation path) from "user passed nothing" (no warning, pick CRS -# from the first source). A plain default of None does not work because -# None is itself a value a caller could supply alongside crs=. -_CRS_WKT_DEPRECATED_SENTINEL = object() # ``open_geotiff`` needs to tell "caller never set missing_sources" (default # sentinel: skip forwarding so the _read_vrt default applies, and reject the # kwarg up front for non-VRT sources) from "caller set missing_sources=" # (forward verbatim to _read_vrt). Mirrors the on_gpu_failure pattern. _MISSING_SOURCES_SENTINEL = object() -# ``_build_vrt`` historically named its first positional kwarg ``vrt_path`` -# while ``to_geotiff`` / ``_write_geotiff_gpu`` use ``path``. The deprecation -# shim adds ``path`` as the new name and accepts ``vrt_path`` with a -# DeprecationWarning. The sentinel pattern distinguishes "user passed -# vrt_path= explicitly" from "user passed nothing", which is the same -# rationale ``_CRS_WKT_DEPRECATED_SENTINEL`` documents above. -_VRT_PATH_DEPRECATED_SENTINEL = object() -# ``_build_vrt`` also needs to distinguish "user passed path= explicitly" -# (including an explicit ``path=None``, which is an error) from "user -# passed nothing" (fall through to the ``vrt_path`` shim). Without this -# sentinel, ``_build_vrt(None, sources)`` silently fell through to the -# ``path is None`` branch and raised a "missing required argument" -# TypeError for the wrong reason. -_VRT_PATH_MISSING_SENTINEL = object() -# ``open_geotiff`` renamed ``mask_nodata`` -> ``masked`` (and flipped the -# default from True to False) and ``name`` -> ``default_name`` to match -# rioxarray's ``open_rasterio``. Each sentinel distinguishes "caller passed -# the deprecated alias" from "caller passed nothing", so passing both the old -# and new name raises TypeError and the old name alone warns. Same rationale -# as ``_GPU_DEPRECATED_SENTINEL`` above. -_MASK_NODATA_DEPRECATED_SENTINEL = object() -_NAME_DEPRECATED_SENTINEL = object() -# ``mask_and_scale`` was renamed to ``unpack`` (the operation unpacks -# CF-packed integers via SCALE/OFFSET). Same sentinel deprecation as the -# pair above: the old name still works but warns, and passing both raises. -_MASK_AND_SCALE_DEPRECATED_SENTINEL = object() - # Spatial dim names recognised on 3D writer inputs. ``y``/``x`` are the # canonical TIFF axes; aliases are accepted so a user who happens to use diff --git a/xrspatial/geotiff/_validation.py b/xrspatial/geotiff/_validation.py index 934f28a9d..04897b5f4 100644 --- a/xrspatial/geotiff/_validation.py +++ b/xrspatial/geotiff/_validation.py @@ -483,11 +483,9 @@ def _validate_dispatch_kwargs( True when the call routes through the GPU pipeline (either ``open_geotiff(gpu=True)`` or a direct ``_read_geotiff_gpu``). This is the dispatch bool, type-checked via - ``_validate_gpu_arg``. Not to be confused with - ``_read_geotiff_gpu``'s own deprecated ``gpu='strict'/'auto'/ - 'loose'`` string parameter (the legacy ``on_gpu_failure`` - alias), which never reaches this helper -- ``_read_geotiff_gpu`` - passes a literal ``True`` here. + ``_validate_gpu_arg``. ``_read_geotiff_gpu`` passes a literal + ``True`` here; its failure policy travels separately as + ``on_gpu_failure``. chunks : int, tuple, or None Caller's ``chunks=`` value. ``None`` means eager. overview_level diff --git a/xrspatial/geotiff/_writers/vrt.py b/xrspatial/geotiff/_writers/vrt.py index 0330caa7c..ec3890faa 100644 --- a/xrspatial/geotiff/_writers/vrt.py +++ b/xrspatial/geotiff/_writers/vrt.py @@ -1,27 +1,20 @@ """Internal VRT writer. -Wraps ``_vrt.write_vrt`` with the ``_build_vrt`` helper: deprecation -handling for the ``crs_wkt`` and ``vrt_path`` aliases, normalisation of +Wraps ``_vrt.write_vrt`` with the ``_build_vrt`` helper: normalisation of the ``crs`` kwarg to WKT via ``_resolve_crs_to_wkt``, and the parity surface vs ``to_geotiff`` / ``_write_geotiff_gpu``. ``to_geotiff``'s ``.vrt`` write path is the only caller; it is not part of the public API. """ from __future__ import annotations -import warnings - from .._crs import _resolve_crs_to_wkt -from .._runtime import (_CRS_WKT_DEPRECATED_SENTINEL, _VRT_PATH_DEPRECATED_SENTINEL, - _VRT_PATH_MISSING_SENTINEL) from .._validation import _validate_nodata_arg -def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, - source_files: list[str] | None = None, *, - vrt_path: str | None = _VRT_PATH_DEPRECATED_SENTINEL, +def _build_vrt(path: str, + source_files: list[str], *, relative: bool = True, crs: int | str | None = None, - crs_wkt: str | None = _CRS_WKT_DEPRECATED_SENTINEL, nodata: float | int | None = None, dst_offsets: list[tuple[int, int]] | None = None) -> str: """Generate a VRT file that mosaics multiple GeoTIFF tiles. @@ -61,13 +54,6 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, shares a single destination-arg name. source_files : list of str [internal-only] Paths to the source GeoTIFF files. - vrt_path : str, optional - [internal-only] Deprecated alias for ``path``. Emits - ``DeprecationWarning`` when supplied; passing both ``path`` - and ``vrt_path`` raises ``TypeError``. Kept so existing - callers (``_build_vrt(vrt_path, sources)`` positional or - ``_build_vrt(vrt_path=...)`` keyword) keep working through the - deprecation window. New code should use ``path``. relative : bool, optional [internal-only] Store source paths relative to the VRT file (default True). @@ -77,16 +63,6 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, the ``crs`` kwarg on ``to_geotiff`` and ``_write_geotiff_gpu`` so the same value can be forwarded to whichever writer the caller picked without per-writer special-casing. - crs_wkt : str or None, optional - [internal-only] Deprecated alias for ``crs``. Emits - ``DeprecationWarning`` when supplied (including - ``crs_wkt=None``); passing both ``crs`` and ``crs_wkt`` raises - ``TypeError``. The value is forwarded through the same - ``_resolve_crs_to_wkt`` path as ``crs``, so any string the - resolver accepts (WKT root keyword, PROJ string, - ``"EPSG:NNNN"``) and ``None`` work here. The historic - ``str | None`` surface is preserved; new code should use - ``crs`` instead, which additionally accepts ``int`` EPSG codes. nodata : float, int, or None, optional [internal-only] NoData value. If None, taken from the first source GeoTIFF. @@ -141,76 +117,20 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, """ # Explicit signature (previously ``**kwargs``) so ``inspect.signature``, # IDE autocomplete, and ``mypy --strict`` can see the accepted kwargs - # without parsing the docstring. Mirrors ``_vrt.write_vrt`` for the - # historic ``crs_wkt`` path; the new ``crs`` path normalises through + # without parsing the docstring. ``crs`` normalises through # ``_resolve_crs_to_wkt`` before forwarding because the internal # writer still only speaks WKT. - # - # The ``path`` / ``vrt_path`` shim resolves the destination kwarg - # before any other processing so the rest of the function works - # uniformly against a single ``vrt_path`` local. ``path`` is the - # new name (parity with to_geotiff / _write_geotiff_gpu); ``vrt_path`` - # is kept as a deprecated alias to preserve back-compat for callers - # using either positional ``_build_vrt(vrt_path, sources)`` or - # keyword ``_build_vrt(vrt_path=...)``. - path_passed = path is not _VRT_PATH_MISSING_SENTINEL - vrt_path_passed = vrt_path is not _VRT_PATH_DEPRECATED_SENTINEL - if path_passed and vrt_path_passed: - # Both supplied is ambiguous regardless of whether the two values - # happen to be the same string. Refuse rather than silently - # picking one. Mirrors the same rule the ``crs`` / ``crs_wkt`` - # shim below applies. - raise TypeError( - "_build_vrt: pass either 'path' or the deprecated 'vrt_path' " - "alias, not both.") - if vrt_path_passed: - warnings.warn( - "_build_vrt(..., vrt_path=...) is deprecated; use path=... " - "instead. The kwarg was renamed for parity with to_geotiff " - "and _write_geotiff_gpu, which already accept 'path' as the " - "destination kwarg.", - DeprecationWarning, - stacklevel=2, - ) - path = vrt_path - elif not path_passed: - # Neither name supplied. Match the previous ``TypeError: missing - # required positional argument`` semantics by raising rather than - # forwarding the sentinel into ``_write_vrt_internal``. - raise TypeError( - "_build_vrt: missing required argument 'path'") if path is None: # Explicit ``path=None`` (including positional ``_build_vrt(None, # sources)``) is rejected up front so the error message names the # offending kwarg instead of crashing deep in - # ``os.path.dirname(os.path.abspath(None))``. The sentinel default - # on ``path`` is what lets us distinguish this case from "caller - # passed nothing" above. + # ``os.path.dirname(os.path.abspath(None))``. raise TypeError( "_build_vrt: 'path' must be a str, got None") if source_files is None: raise TypeError( "_build_vrt: missing required argument 'source_files'") - crs_wkt_passed = crs_wkt is not _CRS_WKT_DEPRECATED_SENTINEL - if crs is not None and crs_wkt_passed: - # Both supplied is ambiguous regardless of whether the WKT happens - # to encode the same CRS as the int. Refuse rather than silently - # picking one. - raise TypeError( - "_build_vrt: pass either 'crs' or the deprecated 'crs_wkt' " - "alias, not both.") - if crs_wkt_passed: - warnings.warn( - "_build_vrt(..., crs_wkt=...) is deprecated; use crs=... " - "instead. The kwarg was renamed for parity with to_geotiff " - "and _write_geotiff_gpu, which already accept 'crs' as either " - "an int EPSG code or a WKT string.", - DeprecationWarning, - stacklevel=2, - ) - crs = crs_wkt - # Reject bool / non-numeric nodata at the entry point so _build_vrt # matches the to_geotiff / _write_geotiff_gpu surface. ``bool`` is a # subclass of ``int`` in Python, so a typo like ``nodata=True`` would diff --git a/xrspatial/geotiff/_xarray_backend.py b/xrspatial/geotiff/_xarray_backend.py index 370fff92d..801632ebd 100644 --- a/xrspatial/geotiff/_xarray_backend.py +++ b/xrspatial/geotiff/_xarray_backend.py @@ -93,8 +93,8 @@ class GeoTIFFBackendEntrypoint(BackendEntrypoint): # xarray's signature introspection (``detect_parameters``) raises on an # ``open_dataset`` that uses ``**kwargs`` without this attribute set. It # also stops xarray from injecting its CF decoders -- in particular - # ``mask_and_scale``, which would collide with open_geotiff's deprecated - # alias of the same name. GeoTIFF read options come in through + # ``mask_and_scale``, which open_geotiff does not accept (its + # equivalent is ``unpack``). GeoTIFF read options come in through # ``backend_kwargs`` instead. open_dataset_parameters = ("filename_or_obj", "drop_variables") diff --git a/xrspatial/geotiff/tests/gpu/test_codec.py b/xrspatial/geotiff/tests/gpu/test_codec.py index 7284da679..6d2f7d60a 100644 --- a/xrspatial/geotiff/tests/gpu/test_codec.py +++ b/xrspatial/geotiff/tests/gpu/test_codec.py @@ -1031,7 +1031,7 @@ def wrapped(*args, **kwargs): path = str(tmp_path / "rgb_jpeg_1549.tif") _write_jpeg_rgb_tiff_1549(path) - arr = _read_geotiff_gpu(path, gpu='strict', allow_internal_only_jpeg=True) + arr = _read_geotiff_gpu(path, on_gpu_failure='strict', allow_internal_only_jpeg=True) assert isinstance(arr.data, cupy.ndarray) decoded = arr.data.get() assert decoded.shape == (256, 256, 3) @@ -1175,7 +1175,7 @@ def _read_cpu_gpu_lerc(path): # The backend default is unmasked (#2976); request masking so the # GPU read promotes the sentinel to NaN to match the CPU mask. gpu_da = _read_geotiff_gpu( - path, gpu='strict', allow_experimental_codecs=True, + path, on_gpu_failure='strict', allow_experimental_codecs=True, mask_nodata=True, ) gpu_host = gpu_da.data.get() diff --git a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py index d45430f07..af83f510c 100644 --- a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +++ b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py @@ -16,9 +16,9 @@ * ``test_gpu_cuda_preflight_1903.py`` -- CUDA preflight in ``_read_geotiff_gpu``. * ``test_gpu_kwarg_rename_1560.py`` -- ``gpu=`` -> ``on_gpu_failure=`` - rename with deprecation shim. -* ``test_gpu_strict_fallback_1516.py`` -- ``gpu='auto'`` warns + - fallbacks; ``gpu='strict'`` re-raises. + rename; the old spelling is rejected. +* ``test_gpu_strict_fallback_1516.py`` -- ``on_gpu_failure='auto'`` + warns + falls back; ``'strict'`` re-raises. * ``test_gpu_fallback_forwards_kwargs_2238.py`` -- CPU-fallback sites in ``_read_geotiff_gpu`` forward caller kwargs. * ``test_kvikio_batched_pread_1688.py`` -- single-allocation batched @@ -212,108 +212,16 @@ def test_on_gpu_failure_invalid_value_raises_value_error_1560(): _read_geotiff_gpu("/nonexistent.tif", on_gpu_failure='loose') -def test_gpu_alias_emits_deprecation_warning_1560(): - """Old ``gpu=`` kwarg still routes through, with a DeprecationWarning.""" +def test_gpu_string_kwarg_removed_1560(): + """The 0.x ``gpu='auto' / 'strict'`` spelling is gone in 1.0.""" from xrspatial.geotiff import _read_geotiff_gpu - with warnings.catch_warnings(record=True) as records: - warnings.simplefilter("always") - # Pass an invalid sentinel so we don't have to mock the full GPU - # pipeline; ValueError fires after the deprecation handler runs. - with pytest.raises(ValueError, match="on_gpu_failure must be"): - _read_geotiff_gpu("/nonexistent.tif", gpu='loose') - - deprecations = [ - r for r in records if issubclass(r.category, DeprecationWarning) - ] - assert deprecations, "expected DeprecationWarning when gpu= is used" - assert "on_gpu_failure" in str(deprecations[0].message) - - -def test_gpu_alias_accepts_old_values_without_validation_error_1560( - monkeypatch): - """``gpu='strict'`` was the legacy spelling; should still validate. - - Stub ``cupy`` with a working preflight so the test exercises the - alias-validation path on every host, including ones where CuPy - imports but the CUDA runtime is unusable (issue #2515). Without the - stub, the real ``_preflight_cuda_runtime`` raises ``RuntimeError`` - on broken-CUDA hosts and the test fails for an environmental - reason that has nothing to do with the alias logic under test. - """ - from xrspatial.geotiff import _read_geotiff_gpu - - # Install a stub cupy whose preflight passes; the call should then - # reach the file-read stage and raise ``FileNotFoundError``. This - # decouples the alias-validation check from real CUDA state. - _install_cupy_stub_1903(monkeypatch, get_device_count=lambda: 1) - - with warnings.catch_warnings(): - # Suppress the deprecation noise; we only care that the value - # passes validation and the call proceeds past the value check. - warnings.simplefilter("ignore", DeprecationWarning) - with pytest.raises( - (FileNotFoundError, OSError, ValueError) - ) as exc_info: - _read_geotiff_gpu("/nonexistent.tif", gpu='strict') - - # The validation ValueError carries our exact message; a generic - # file-read failure is fine because it means validation passed. - if isinstance(exc_info.value, ValueError): - assert "on_gpu_failure must be" not in str(exc_info.value) - - -def test_passing_both_raises_type_error_1560(): - """Mixing the new and deprecated names is ambiguous; refuse.""" - from xrspatial.geotiff import _read_geotiff_gpu - - with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"): - _read_geotiff_gpu( - "/nonexistent.tif", - on_gpu_failure='strict', - gpu='auto', - ) - - -@pytest.mark.parametrize("on_gpu_failure_val,gpu_val", [ - ('auto', 'strict'), - ('auto', 'auto'), - ('strict', 'strict'), -]) -def test_passing_both_raises_regardless_of_values_1560( - on_gpu_failure_val, gpu_val): - """Both-supplied is rejected even when ``on_gpu_failure='auto'``. - - A sentinel-based detection (rather than ``!= 'auto'``) catches the - case where the caller passes the default value explicitly alongside - the deprecated alias. - """ - from xrspatial.geotiff import _read_geotiff_gpu - - with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"): - _read_geotiff_gpu( - "/nonexistent.tif", - on_gpu_failure=on_gpu_failure_val, - gpu=gpu_val, - ) - - -def test_gpu_alias_bool_no_longer_misleading_value_error_1560(): - """Calling with ``gpu=True`` -- the documented bool from the - dispatchers -- used to raise ``ValueError: gpu must be 'auto' or - 'strict', got True``. The new error explicitly names - ``on_gpu_failure`` so the rename is discoverable from the traceback. - """ - from xrspatial.geotiff import _read_geotiff_gpu - - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - with pytest.raises(ValueError, match="on_gpu_failure must be"): - _read_geotiff_gpu("/nonexistent.tif", gpu=True) + with pytest.raises(TypeError, match="gpu"): + _read_geotiff_gpu("/nonexistent.tif", gpu='strict') # ===================================================================== -# Section 1516 -- ``gpu='auto'`` warns + falls back; ``'strict'`` raises +# Section 1516 -- ``on_gpu_failure='auto'`` warns + falls back; ``'strict'`` raises # ===================================================================== # # ``_read_geotiff_gpu`` previously wrapped the GPU decode in a too-broad @@ -323,10 +231,10 @@ def test_gpu_alias_bool_no_longer_misleading_value_error_1560(): # was still numerically correct. # # The fix: -# 1. Default ``gpu='auto'`` still falls back to CPU, but emits a +# 1. Default ``on_gpu_failure='auto'`` still falls back to CPU, but emits a # ``RuntimeWarning`` reporting the original exception type and # message so failures are visible. -# 2. New ``gpu='strict'`` mode re-raises instead of falling back, so +# 2. ``on_gpu_failure='strict'`` re-raises instead of falling back, so # tests and CI for the GPU fast path see real errors. # # These tests monkeypatch ``gpu_decode_tiles_from_file`` to raise a @@ -466,7 +374,7 @@ def _boom(*args, **kwargs): def test_default_mode_warns_on_gpu_failure_1516(tiled_tiff_path_1516, monkeypatch): - """Default ``gpu='auto'`` warns and falls back to the CPU result.""" + """Default ``on_gpu_failure='auto'`` warns and falls back to the CPU result.""" inserted_stub = _ensure_cupy_stub_1516() try: from xrspatial.geotiff import _read_geotiff_gpu @@ -492,7 +400,7 @@ def test_default_mode_warns_on_gpu_failure_1516(tiled_tiff_path_1516, monkeypatc def test_strict_mode_reraises_1516(tiled_tiff_path_1516, monkeypatch): - """``gpu='strict'`` re-raises the original GPU exception.""" + """``on_gpu_failure='strict'`` re-raises the original GPU exception.""" inserted_stub = _ensure_cupy_stub_1516() try: from xrspatial.geotiff import _read_geotiff_gpu @@ -503,7 +411,7 @@ def test_strict_mode_reraises_1516(tiled_tiff_path_1516, monkeypatch): _patch_gpu_decode_to_raise_1516(monkeypatch, synthetic) with pytest.raises(RuntimeError, match="simulated GPU failure"): - _read_geotiff_gpu(path, gpu='strict') + _read_geotiff_gpu(path, on_gpu_failure='strict') finally: if inserted_stub: _restore_cupy_1516() @@ -511,7 +419,7 @@ def test_strict_mode_reraises_1516(tiled_tiff_path_1516, monkeypatch): def test_strict_mode_reraises_second_stage_1516( tiled_tiff_path_1516, monkeypatch): - """``gpu='strict'`` re-raises if the second-stage GPU decode fails too. + """``on_gpu_failure='strict'`` re-raises if the second-stage GPU decode fails too. Regression for the case where ``gpu_decode_tiles_from_file`` and the follow-up ``gpu_decode_tiles`` both fail. Previously the second @@ -529,7 +437,7 @@ def test_strict_mode_reraises_second_stage_1516( with pytest.raises(RuntimeError, match="simulated second-stage GPU failure"): - _read_geotiff_gpu(path, gpu='strict') + _read_geotiff_gpu(path, on_gpu_failure='strict') finally: if inserted_stub: _restore_cupy_1516() @@ -537,7 +445,7 @@ def test_strict_mode_reraises_second_stage_1516( def test_default_mode_warns_on_second_stage_failure_1516( tiled_tiff_path_1516, monkeypatch): - """``gpu='auto'`` warns once per stage failure and falls back to CPU. + """``on_gpu_failure='auto'`` warns once per stage failure and falls back to CPU. Both GPU decode stages are forced to raise, so the user sees two distinct ``RuntimeWarning`` records (one per stage) before the CPU @@ -584,14 +492,9 @@ def test_invalid_gpu_kwarg_rejected_1516(tiled_tiff_path_1516): path, _ = tiled_tiff_path_1516 - # The kwarg was renamed to ``on_gpu_failure``; the - # validation error now names the new kwarg even when callers - # supply the deprecated ``gpu=`` alias. with pytest.raises(ValueError, match="on_gpu_failure must be 'auto' or 'strict'"): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - _read_geotiff_gpu(path, gpu='loose') + _read_geotiff_gpu(path, on_gpu_failure='loose') finally: if inserted_stub: _restore_cupy_1516() diff --git a/xrspatial/geotiff/tests/integration/test_dask_pipeline.py b/xrspatial/geotiff/tests/integration/test_dask_pipeline.py index 471ffa735..42fb6b8ee 100644 --- a/xrspatial/geotiff/tests/integration/test_dask_pipeline.py +++ b/xrspatial/geotiff/tests/integration/test_dask_pipeline.py @@ -1019,7 +1019,7 @@ def test_no_coords_raises(self, tmp_path): ds.xrs.open_geotiff(path) def test_kwargs_forwarded(self, tmp_path): - """Extra kwargs like name= should be forwarded to open_geotiff.""" + """Extra kwargs like default_name= should be forwarded to open_geotiff.""" da = _make_da_accessor_io(height=8, width=10) path = str(tmp_path / 'test_1047_kwargs.tif') to_geotiff(da, path, compression='none') @@ -1032,7 +1032,7 @@ def test_kwargs_forwarded(self, tmp_path): 'x': da.coords['x'].values}, ) }) - result = template.xrs.open_geotiff(path, name='myname') + result = template.xrs.open_geotiff(path, default_name='myname') assert result.name == 'myname' @@ -1082,7 +1082,7 @@ def test_kwargs_forwarded(self, tmp_path): 'x': da.coords['x'].values}, attrs={'crs': 4326}, ) - result = template.xrs.open_geotiff(path, name='myname') + result = template.xrs.open_geotiff(path, default_name='myname') assert result.name == 'myname' diff --git a/xrspatial/geotiff/tests/parity/test_finalization.py b/xrspatial/geotiff/tests/parity/test_finalization.py index 95599b043..d94bca4fa 100644 --- a/xrspatial/geotiff/tests/parity/test_finalization.py +++ b/xrspatial/geotiff/tests/parity/test_finalization.py @@ -15,7 +15,7 @@ Section 2 -- Eager finalization parity ``_finalize_eager_read`` stamps the same nodata / georef attrs on the eager numpy and eager GPU paths. The matrix walks float / int / - out-of-range sentinels, ``mask_nodata=False``, no-sentinel, + out-of-range sentinels, ``masked=False``, no-sentinel, explicit ``dtype=``, windowed reads, MinIsWhite, and multi-band. Section 3 -- Lazy finalization parity @@ -636,13 +636,13 @@ def test_int_out_of_range_sentinel_is_no_op(tmp_path): @requires_gpu def test_mask_nodata_false_keeps_literal_sentinel(tmp_path): - """mask_nodata=False leaves the buffer untouched on both backends.""" + """masked=False leaves the buffer untouched on both backends.""" arr = np.array( [[1.0, 2.0, -9999.0], [4.0, -9999.0, 6.0]], dtype=np.float32) path = str(tmp_path / 'eager_parity_2179_mask_false.tif') _write_with_nodata(arr, path, nodata=-9999.0) - cpu, gpu = _read_both(path, mask_nodata=False) + cpu, gpu = _read_both(path, masked=False) # No NaN substitution; the literal sentinel survives on both # backends with ``masked_nodata=False``. diff --git a/xrspatial/geotiff/tests/parity/test_signature_contract.py b/xrspatial/geotiff/tests/parity/test_signature_contract.py index a588d0811..d241a0aa8 100644 --- a/xrspatial/geotiff/tests/parity/test_signature_contract.py +++ b/xrspatial/geotiff/tests/parity/test_signature_contract.py @@ -54,32 +54,20 @@ def test_write_vrt_signature_exposes_documented_kwargs(): - """``inspect.signature(_build_vrt)`` reports the four accepted kwargs. + """``inspect.signature(_build_vrt)`` reports the documented kwargs. When the public wrapper used ``**kwargs``, ``inspect.signature`` - only saw ``vrt_path`` and ``source_files``. ``crs`` was added for - parity with ``to_geotiff`` / ``_write_geotiff_gpu`` while keeping the - historic ``crs_wkt`` as a deprecated alias (sentinel default so the - deprecation shim can tell "user passed nothing" from "user passed - crs_wkt=None"). + only saw the two positional arguments. """ sig = inspect.signature(_build_vrt) params = sig.parameters assert 'relative' in params assert 'crs' in params # canonical kwarg - assert 'crs_wkt' in params # deprecated alias assert 'nodata' in params assert params['relative'].default is True # ``crs`` is the new canonical kwarg; default None means "pick from # the first source", matching to_geotiff / _write_geotiff_gpu. assert params['crs'].default is None - # ``crs_wkt`` carries a sentinel default so the deprecation shim - # can distinguish "user passed nothing" (no warning) from "user - # passed crs_wkt=None" (deprecated-but-explicit, warn). The - # sentinel itself is private; check that it is NOT None so a - # future maintainer cannot accidentally drop the sentinel logic. - assert params['crs_wkt'].default is not None - assert params['crs_wkt'].default is not inspect.Parameter.empty assert params['nodata'].default is None # No catch-all VAR_KEYWORD kinds = {p.kind for p in params.values()} @@ -106,8 +94,7 @@ def test_write_vrt_unknown_kwarg_rejected_at_public_level(tmp_path): def test_write_vrt_accepts_documented_kwargs(tmp_path): """Each documented kwarg round-trips through the explicit signature. - Uses the canonical ``crs=None`` kwarg form. The deprecated - ``crs_wkt`` alias is exercised separately. + Uses the canonical ``crs=None`` kwarg form. """ arr = np.zeros((8, 8), dtype=np.float32) da = xr.DataArray( diff --git a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py index 8f3266c74..17e5e2e9d 100644 --- a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py +++ b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py @@ -138,20 +138,11 @@ def test_masked_true_promotes_and_masks(tmp_path): assert out.attrs.get("masked_nodata") is True -def test_mask_nodata_alias_warns_and_matches(tmp_path): +def test_mask_nodata_removed(tmp_path): + """The 0.x ``mask_nodata=`` alias is gone in 1.0; use ``masked=``.""" path = _int_sentinel_tiff(str(tmp_path / "t2961_alias.tif")) - with pytest.warns(DeprecationWarning, match="mask_nodata.*deprecated"): - legacy = open_geotiff(path, mask_nodata=True) - canonical = open_geotiff(path, masked=True) - np.testing.assert_array_equal( - np.isnan(legacy.data), np.isnan(canonical.data)) - assert legacy.dtype == canonical.dtype == np.float64 - - -def test_masked_and_mask_nodata_both_raises(tmp_path): - path = _int_sentinel_tiff(str(tmp_path / "t2961_both.tif")) - with pytest.raises(TypeError, match="either 'masked' or"): - open_geotiff(path, masked=True, mask_nodata=True) + with pytest.raises(TypeError, match="mask_nodata"): + open_geotiff(path, mask_nodata=True) def test_canonical_masked_false_emits_no_warning(tmp_path, recwarn): @@ -162,7 +153,7 @@ def test_canonical_masked_false_emits_no_warning(tmp_path, recwarn): # --------------------------------------------------------------------------- -# default_name / name deprecation alias +# default_name (the 0.x ``name=`` alias was removed in 1.0) # --------------------------------------------------------------------------- def test_default_name_sets_array_name(tmp_path): @@ -171,38 +162,21 @@ def test_default_name_sets_array_name(tmp_path): assert out.name == "elevation" -def test_name_alias_warns_and_matches(tmp_path): +def test_name_removed(tmp_path): path = _int_sentinel_tiff(str(tmp_path / "t2961_name_alias.tif")) - with pytest.warns(DeprecationWarning, match="name.*deprecated"): - out = open_geotiff(path, name="elevation") - assert out.name == "elevation" - - -def test_default_name_and_name_both_raises(tmp_path): - path = _int_sentinel_tiff(str(tmp_path / "t2961_name_both.tif")) - with pytest.raises(TypeError, match="either 'default_name' or"): - open_geotiff(path, default_name="a", name="b") + with pytest.raises(TypeError, match="name"): + open_geotiff(path, name="elevation") # --------------------------------------------------------------------------- # unpack (renamed from mask_and_scale) # --------------------------------------------------------------------------- -def test_mask_and_scale_alias_warns_and_matches(tmp_path): - """The deprecated ``mask_and_scale`` alias warns and matches ``unpack``.""" +def test_mask_and_scale_removed(tmp_path): + """The 0.x ``mask_and_scale=`` alias is gone in 1.0; use ``unpack=``.""" path = _scale_offset_tiff(str(tmp_path / "t3071_ms_alias.tif")) - with pytest.warns(DeprecationWarning, match="mask_and_scale.*deprecated"): - legacy = open_geotiff(path, mask_and_scale=True) - canonical = open_geotiff(path, unpack=True) - np.testing.assert_array_equal(legacy.data, canonical.data) - assert legacy.attrs.get("scale_factor") == canonical.attrs.get( - "scale_factor") == 2.0 - - -def test_unpack_and_mask_and_scale_both_raises(tmp_path): - path = _scale_offset_tiff(str(tmp_path / "t3071_ms_both.tif")) - with pytest.raises(TypeError, match="either 'unpack' or"): - open_geotiff(path, unpack=True, mask_and_scale=True) + with pytest.raises(TypeError, match="mask_and_scale"): + open_geotiff(path, mask_and_scale=True) def test_unpack_false_emits_no_warning(tmp_path, recwarn): diff --git a/xrspatial/geotiff/tests/release_gates/test_features.py b/xrspatial/geotiff/tests/release_gates/test_features.py index 00d9f98d6..ae00d7684 100644 --- a/xrspatial/geotiff/tests/release_gates/test_features.py +++ b/xrspatial/geotiff/tests/release_gates/test_features.py @@ -2626,27 +2626,6 @@ def test_xrs_plot_no_palette(self, tmp_path): import matplotlib.pyplot as plt plt.close('all') - def test_plot_geotiff_deprecated(self, tmp_path): - """plot_geotiff still works but emits a DeprecationWarning.""" - import matplotlib - matplotlib.use('Agg') - import xrspatial.accessor # noqa: F401 - from xrspatial.geotiff import plot_geotiff - - palette = [(65535, 0, 0), (0, 65535, 0)] + [(0, 0, 0)] * 254 - pixels = np.array([[0, 1], [1, 0]], dtype=np.uint8) - tiff_data = _make_palette_tiff(2, 2, 8, pixels, palette) - path = str(tmp_path / 'deprecated.tif') - with open(path, 'wb') as f: - f.write(tiff_data) - - da = open_geotiff(path) - with pytest.warns(DeprecationWarning, match='plot_geotiff is deprecated'): - artist = plot_geotiff(da) - assert artist is not None - import matplotlib.pyplot as plt - plt.close('all') - def test_non_palette_no_cmap(self, tmp_path): """Non-palette TIFFs should not have any colormap attr. @@ -2911,12 +2890,12 @@ def test_star_import_brings_in_all_public_names(self): for name in g.__all__: assert name in ns, f"{name} listed in __all__ but not exported" - def test_plot_geotiff_not_in_all_but_still_importable(self): - # plot_geotiff is intentionally omitted from __all__ (deprecated) - # but stays importable so existing user code keeps working. + def test_plot_geotiff_removed(self): + # The deprecated plot_geotiff wrapper was removed in 1.0; the + # replacement is da.xrs.plot(). import xrspatial.geotiff as g assert 'plot_geotiff' not in g.__all__ - assert hasattr(g, 'plot_geotiff') + assert not hasattr(g, 'plot_geotiff') # =========================================================================== diff --git a/xrspatial/geotiff/tests/release_gates/test_stable_features.py b/xrspatial/geotiff/tests/release_gates/test_stable_features.py index 68bdc52d7..523a7445e 100644 --- a/xrspatial/geotiff/tests/release_gates/test_stable_features.py +++ b/xrspatial/geotiff/tests/release_gates/test_stable_features.py @@ -1316,7 +1316,7 @@ def test_release_gate_codec_round_trip( source = _round_trip_make_input(dtype_name) is_float = np.issubdtype(np.dtype(dtype_name), np.floating) - mask_kwargs: dict = {} if is_float else {"mask_nodata": False} + mask_kwargs: dict = {} if is_float else {"masked": False} pass_one_kwargs: dict = ( {} if is_float else {"nodata": source.attrs["nodata"]} diff --git a/xrspatial/geotiff/tests/unit/test_metadata.py b/xrspatial/geotiff/tests/unit/test_metadata.py index afaec480c..0053f25a2 100644 --- a/xrspatial/geotiff/tests/unit/test_metadata.py +++ b/xrspatial/geotiff/tests/unit/test_metadata.py @@ -1349,19 +1349,10 @@ def test_read_geotiff_dask_band_nodata_rejects_unknown_value_1987(tmp_path): # =========================================================================== -def test_gpu_deprecated_sentinel_is_singleton(): - assert geotiff_pkg._GPU_DEPRECATED_SENTINEL is _runtime._GPU_DEPRECATED_SENTINEL - - def test_on_gpu_failure_sentinel_is_singleton(): assert geotiff_pkg._ON_GPU_FAILURE_SENTINEL is _runtime._ON_GPU_FAILURE_SENTINEL -def test_crs_wkt_deprecated_sentinel_is_singleton(): - assert geotiff_pkg._CRS_WKT_DEPRECATED_SENTINEL is \ - _runtime._CRS_WKT_DEPRECATED_SENTINEL - - def test_missing_sources_sentinel_is_singleton(): assert geotiff_pkg._MISSING_SOURCES_SENTINEL is \ _runtime._MISSING_SOURCES_SENTINEL diff --git a/xrspatial/geotiff/tests/write/test_basic.py b/xrspatial/geotiff/tests/write/test_basic.py index c6dd375ab..32c028b3d 100644 --- a/xrspatial/geotiff/tests/write/test_basic.py +++ b/xrspatial/geotiff/tests/write/test_basic.py @@ -897,14 +897,11 @@ def test_write_vrt_signature_first_arg_is_path(): """ sig = inspect.signature(_build_vrt) params = list(sig.parameters) - # ``path`` is the new canonical name, ``source_files`` follows. - # ``vrt_path`` is kept as a keyword-only deprecated alias. + # ``path`` is the canonical name, ``source_files`` follows. The 0.x + # ``vrt_path`` alias was removed in 1.0. assert params[0] == 'path' assert params[1] == 'source_files' - assert 'vrt_path' in params - # ``vrt_path`` is keyword-only (the alias should never be used - # positionally going forward). - assert sig.parameters['vrt_path'].kind == inspect.Parameter.KEYWORD_ONLY + assert 'vrt_path' not in params def test_write_vrt_positional_path_works(tmp_path): @@ -928,8 +925,7 @@ def test_write_vrt_path_kwarg_works(tmp_path): A caller who passes everything by keyword (no positional args) previously could not reach the function because the ``path`` kwarg - did not exist; this is the path-symmetric counterpart to the existing - ``_build_vrt(vrt_path=...)`` test below. + did not exist. """ src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') @@ -940,57 +936,24 @@ def test_write_vrt_path_kwarg_works(tmp_path): assert os.path.exists(out) -def test_write_vrt_vrt_path_kwarg_emits_deprecation_warning(tmp_path): - """``vrt_path=...`` works but emits ``DeprecationWarning``. - - Mirrors the existing ``crs_wkt`` deprecation in the same writer: - old name still works, but caller sees a clear migration - hint via the warning. - """ +def test_write_vrt_vrt_path_kwarg_removed(tmp_path): + """The old ``vrt_path=`` spelling is gone in 1.0; use ``path=``.""" src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') - with pytest.warns(DeprecationWarning, match='vrt_path'): - result = _build_vrt(vrt_path=out, source_files=[src]) - assert result == out - assert os.path.exists(out) - - -def test_write_vrt_path_and_vrt_path_together_raises(tmp_path): - """Both names supplied is ambiguous; refuse to pick one. - - Mirrors the ``crs`` / ``crs_wkt`` rule documented in the existing - _build_vrt source: passing both is rejected with TypeError - regardless of whether the two values happen to match. - """ - src = _build_source_tif(tmp_path) - out = str(tmp_path / 'out.vrt') - with pytest.raises(TypeError, match="path.*vrt_path"): - _build_vrt(path=out, vrt_path=out, source_files=[src]) + with pytest.raises(TypeError, match='vrt_path'): + _build_vrt(vrt_path=out, source_files=[src]) def test_write_vrt_no_path_raises(tmp_path): - """Neither ``path`` nor ``vrt_path`` -> TypeError. - - Before the shim, omitting the first positional argument raised - ``TypeError: missing 1 required positional argument`` from CPython. - The shim adds a sentinel default so the kwarg-only positional no - longer triggers that automatic check; the explicit raise inside - the shim preserves the original error semantics. - """ + """Omitting ``path`` raises a TypeError that names the argument.""" src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match='path'): _build_vrt(source_files=[src]) def test_write_vrt_explicit_path_none_raises(tmp_path): - """``_build_vrt(path=None, ...)`` is rejected with TypeError. - - The sentinel-default pattern distinguishes "caller - passed nothing" (sentinel) from "caller passed None explicitly". - Explicit ``None`` is a bug in the caller's code, not a request to - fall through to the deprecated ``vrt_path`` alias, so the shim - raises with a clear message that names the offending kwarg. - """ + """``_build_vrt(path=None, ...)`` is rejected with a TypeError that + names the offending kwarg.""" src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match="'path'.*None"): _build_vrt(path=None, source_files=[src]) @@ -1032,29 +995,6 @@ def test_write_vrt_first_arg_name_matches_writer_trio(): assert vrt_first == 'path' -def test_write_vrt_path_round_trip_matches_old(tmp_path): - """The written VRT decodes the same regardless of which kwarg name - the caller used. - - Smoke test that the shim does not silently drop or re-route any of - the other kwargs while resolving ``path`` vs ``vrt_path``. - """ - src = _build_source_tif(tmp_path) - out_new = str(tmp_path / 'out_new.vrt') - out_old = str(tmp_path / 'out_old.vrt') - - _build_vrt(out_new, [src]) - with warnings.catch_warnings(): - # ignore the deprecation; we still need the legacy path to - # produce a byte-identical mosaic. - warnings.simplefilter('ignore', DeprecationWarning) - _build_vrt(vrt_path=out_old, source_files=[src]) - - a_new = _read_vrt(out_new) - a_old = _read_vrt(out_old) - np.testing.assert_array_equal(np.asarray(a_new), np.asarray(a_old)) - - # ------------------------------------------------------------------------- # Section: _build_vrt CRS propagation # ------------------------------------------------------------------------- @@ -1150,12 +1090,11 @@ def test_write_vrt_no_crs_kwarg_no_warning(tmp_path): assert os.path.exists(vrt_path) -# --- Deprecation shim: ``crs_wkt=`` still works but warns --- +# --- ``crs_wkt=`` was removed in 1.0; ``crs=`` takes WKT too --- -def test_write_vrt_crs_wkt_deprecated_warns(tmp_path): - """Passing ``crs_wkt=`` emits ``DeprecationWarning`` but still - produces a working VRT.""" +def test_write_vrt_crs_wkt_removed(tmp_path): + """The old ``crs_wkt=`` spelling is gone; pass the WKT via ``crs=``.""" src = _build_source_tif(tmp_path, 'depr.tif') vrt_path = str(tmp_path / 'depr.vrt') @@ -1163,40 +1102,14 @@ def test_write_vrt_crs_wkt_deprecated_warns(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() - with pytest.warns(DeprecationWarning, match='crs_wkt'): - out = _build_vrt(vrt_path, [src], crs_wkt=wkt) + with pytest.raises(TypeError, match='crs_wkt'): + _build_vrt(vrt_path, [src], crs_wkt=wkt) + out = _build_vrt(vrt_path, [src], crs=wkt) assert out == vrt_path da = _read_vrt(vrt_path) assert da.attrs.get('crs') == 4326 -def test_write_vrt_crs_wkt_none_still_warns(tmp_path): - """``crs_wkt=None`` (explicit) was a documented shape in the old - signature -- it now warns because the caller is using the - deprecated kwarg name, even if the value is None.""" - src = _build_source_tif(tmp_path, 'depr_none.tif') - vrt_path = str(tmp_path / 'depr_none.vrt') - - with pytest.warns(DeprecationWarning, match='crs_wkt'): - _build_vrt(vrt_path, [src], crs_wkt=None) - assert os.path.exists(vrt_path) - - -def test_write_vrt_both_crs_and_crs_wkt_rejected(tmp_path): - """Passing both raises ``TypeError`` rather than silently picking - one. The error message names both kwargs so the caller can fix - their call quickly.""" - src = _build_source_tif(tmp_path, 'both.tif') - vrt_path = str(tmp_path / 'both.vrt') - - from pyproj import CRS - - wkt = CRS.from_epsg(4326).to_wkt() - - with pytest.raises(TypeError, match='crs.*crs_wkt'): - _build_vrt(vrt_path, [src], crs=4326, crs_wkt=wkt) - - # --- Cross-writer parity: same kwarg name on all three writers --- diff --git a/xrspatial/geotiff/tests/write/test_cog.py b/xrspatial/geotiff/tests/write/test_cog.py index 1e52b479e..24fd145fe 100644 --- a/xrspatial/geotiff/tests/write/test_cog.py +++ b/xrspatial/geotiff/tests/write/test_cog.py @@ -132,7 +132,7 @@ def test_open_geotiff_custom_name(self, tmp_path): path = str(tmp_path / 'test.tif') write(arr, path, compression='none', tiled=False) - da = open_geotiff(path, name='custom') + da = open_geotiff(path, default_name='custom') assert da.name == 'custom' def test_write_numpy_array(self, tmp_path): diff --git a/xrspatial/geotiff/tests/write/test_nodata.py b/xrspatial/geotiff/tests/write/test_nodata.py index 1e5c54fd3..5be28be0e 100644 --- a/xrspatial/geotiff/tests/write/test_nodata.py +++ b/xrspatial/geotiff/tests/write/test_nodata.py @@ -11,7 +11,7 @@ round-trips. * an out-of-range integer sentinel (e.g. uint16 + ``-9999``) is a value-match no-op rather than an ``OverflowError``. -* ``mask_nodata=False`` keeps the source integer dtype so the +* ``masked=False`` keeps the source integer dtype so the ``dtype=`` cast contract is reachable. GPU-only nodata cases live with the GPU tests; the GPU parity tests @@ -606,7 +606,7 @@ def test_apply_nodata_mask_gpu_out_of_range_no_crash(): # =========================================================================== -# ``mask_nodata=False`` keeps the source integer dtype +# ``masked=False`` keeps the source integer dtype # =========================================================================== @@ -647,7 +647,7 @@ def test_regression_dtype_uint16_was_unreachable( """Without the kwarg, ``dtype="uint16"`` raises on a matching sentinel. Baseline that documents the broken contract: this is the original - failure mode reported in the issue. The ``mask_nodata=False`` + failure mode reported in the issue. The ``masked=False`` branch below is the fix. """ path, _ = uint16_with_matching_sentinel @@ -656,9 +656,9 @@ def test_regression_dtype_uint16_was_unreachable( def test_mask_nodata_false_preserves_uint16(uint16_with_matching_sentinel): - """``mask_nodata=False`` keeps the uint16 source dtype.""" + """``masked=False`` keeps the uint16 source dtype.""" path, arr = uint16_with_matching_sentinel - da = open_geotiff(path, dtype='uint16', mask_nodata=False) + da = open_geotiff(path, dtype='uint16', masked=False) assert da.dtype == np.uint16 # Raw sentinels survive in the data. np.testing.assert_array_equal(da.values, arr) @@ -669,7 +669,7 @@ def test_mask_nodata_false_preserves_uint16(uint16_with_matching_sentinel): def test_mask_nodata_false_no_dtype_kwarg(uint16_with_matching_sentinel): """Without ``dtype=``, the source dtype is preserved as-is.""" path, arr = uint16_with_matching_sentinel - da = open_geotiff(path, mask_nodata=False) + da = open_geotiff(path, masked=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.values, arr) assert da.attrs['nodata'] == 0 @@ -705,7 +705,7 @@ def test_no_match_both_modes_agree(uint16_no_match): """ path, arr = uint16_no_match masked = open_geotiff(path) - unmasked = open_geotiff(path, mask_nodata=False) + unmasked = open_geotiff(path, masked=False) assert masked.dtype == np.uint16 assert unmasked.dtype == np.uint16 np.testing.assert_array_equal(masked.values, arr) @@ -713,13 +713,13 @@ def test_no_match_both_modes_agree(uint16_no_match): def test_float_file_mask_nodata_false_keeps_data(float32_tiff): - """For a float32 file with NaN nodata, ``mask_nodata=False`` is a + """For a float32 file with NaN nodata, ``masked=False`` is a no-op: the sentinel is NaN so the inline mask would do nothing anyway, and the float dtype is preserved either way. """ path, arr = float32_tiff masked = open_geotiff(path) - unmasked = open_geotiff(path, mask_nodata=False) + unmasked = open_geotiff(path, masked=False) assert masked.dtype == np.float32 assert unmasked.dtype == np.float32 np.testing.assert_array_equal(np.isnan(masked.values), @@ -731,7 +731,7 @@ def test_float_file_mask_nodata_false_keeps_data(float32_tiff): def test_dtype_cast_preservation_uint8(tmp_path): """Casting to a different integer dtype also works with the opt-out. - The reader keeps the source dtype (uint16) via ``mask_nodata=False``, + The reader keeps the source dtype (uint16) via ``masked=False``, then ``dtype="uint32"`` casts integer-to-integer, which is allowed. """ arr = np.array([[0, 100, 200], @@ -739,7 +739,7 @@ def test_dtype_cast_preservation_uint8(tmp_path): path = str(tmp_path / 'uint16_to_uint32_2052.tif') write(arr, path, nodata=0, compression='none', tiled=False) - da = open_geotiff(path, dtype='uint32', mask_nodata=False) + da = open_geotiff(path, dtype='uint32', masked=False) assert da.dtype == np.uint32 np.testing.assert_array_equal(da.values, arr.astype(np.uint32)) @@ -751,7 +751,7 @@ def test_dask_path_mask_nodata_false(uint16_with_matching_sentinel): graph dtype to float64 and force the per-chunk cast. """ path, arr = uint16_with_matching_sentinel - da = open_geotiff(path, chunks=2, mask_nodata=False) + da = open_geotiff(path, chunks=2, masked=False) assert da.dtype == np.uint16 computed = da.compute() assert computed.dtype == np.uint16 @@ -782,8 +782,8 @@ def test_dask_path_default_still_promotes(uint16_with_matching_sentinel): def test_dask_dtype_cast_with_opt_out(uint16_with_matching_sentinel): - """``dtype="uint16"`` + ``mask_nodata=False`` works on the dask path.""" + """``dtype="uint16"`` + ``masked=False`` works on the dask path.""" path, arr = uint16_with_matching_sentinel - da = open_geotiff(path, chunks=2, dtype='uint16', mask_nodata=False) + da = open_geotiff(path, chunks=2, dtype='uint16', masked=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.compute().values, arr) From dd2f02d594feb4f79f785c3b6bd70f1b8d928f92 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:24:40 -0500 Subject: [PATCH 2/4] Address review: read on_gpu_failure directly and pin removal-test messages (#3759) --- xrspatial/geotiff/_backends/gpu.py | 13 ++++++------- xrspatial/geotiff/_writers/vrt.py | 2 ++ .../geotiff/tests/gpu/test_kernels_and_kwargs.py | 2 +- .../tests/read/test_rioxarray_compat_2961.py | 6 +++--- xrspatial/geotiff/tests/write/test_basic.py | 4 ++-- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/xrspatial/geotiff/_backends/gpu.py b/xrspatial/geotiff/_backends/gpu.py index 216607337..ba712844b 100644 --- a/xrspatial/geotiff/_backends/gpu.py +++ b/xrspatial/geotiff/_backends/gpu.py @@ -297,11 +297,10 @@ def _read_geotiff_gpu(source: str, *, if on_gpu_failure is _ON_GPU_FAILURE_SENTINEL: on_gpu_failure = 'auto' - # The failure policy is read below through the ``gpu`` local. - gpu = on_gpu_failure - if gpu not in ('auto', 'strict'): + if on_gpu_failure not in ('auto', 'strict'): raise ValueError( - f"on_gpu_failure must be 'auto' or 'strict', got {gpu!r}") + "on_gpu_failure must be 'auto' or 'strict', " + f"got {on_gpu_failure!r}") # Reject non-positive chunk sizes up front so the GPU dask+cupy path # surfaces the same error as ``_read_geotiff_dask``. Previously # ``chunks=0`` raised ``ZeroDivisionError`` deep in cupy/dask, and @@ -770,7 +769,7 @@ def _read_once(): tw, th, width, height, compression, predictor, file_dtype, byte_order=ifd_header.byte_order, - gpu=gpu, + gpu=on_gpu_failure, ) if band_arr is None: # Auto-mode signal: stage-2 GPU decode failed for this @@ -843,7 +842,7 @@ def _read_once(): masked_fill=masked_fill, ) except Exception as e: - if gpu == 'strict' or _geotiff_strict_mode(): + if on_gpu_failure == 'strict' or _geotiff_strict_mode(): raise warnings.warn( f"_read_geotiff_gpu: GPU decode failed " @@ -882,7 +881,7 @@ def _read_once(): masked_fill=masked_fill, ) except Exception as e: - if gpu == 'strict' or _geotiff_strict_mode(): + if on_gpu_failure == 'strict' or _geotiff_strict_mode(): raise warnings.warn( f"_read_geotiff_gpu: GPU decode failed " diff --git a/xrspatial/geotiff/_writers/vrt.py b/xrspatial/geotiff/_writers/vrt.py index ec3890faa..caf7a8a98 100644 --- a/xrspatial/geotiff/_writers/vrt.py +++ b/xrspatial/geotiff/_writers/vrt.py @@ -128,6 +128,8 @@ def _build_vrt(path: str, raise TypeError( "_build_vrt: 'path' must be a str, got None") if source_files is None: + # Only reachable via an explicit ``source_files=None``; omitting + # the argument is caught by CPython before we get here. raise TypeError( "_build_vrt: missing required argument 'source_files'") diff --git a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py index af83f510c..71166284b 100644 --- a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +++ b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py @@ -216,7 +216,7 @@ def test_gpu_string_kwarg_removed_1560(): """The 0.x ``gpu='auto' / 'strict'`` spelling is gone in 1.0.""" from xrspatial.geotiff import _read_geotiff_gpu - with pytest.raises(TypeError, match="gpu"): + with pytest.raises(TypeError, match="unexpected keyword argument 'gpu'"): _read_geotiff_gpu("/nonexistent.tif", gpu='strict') diff --git a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py index 17e5e2e9d..c59f70092 100644 --- a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py +++ b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py @@ -141,7 +141,7 @@ def test_masked_true_promotes_and_masks(tmp_path): def test_mask_nodata_removed(tmp_path): """The 0.x ``mask_nodata=`` alias is gone in 1.0; use ``masked=``.""" path = _int_sentinel_tiff(str(tmp_path / "t2961_alias.tif")) - with pytest.raises(TypeError, match="mask_nodata"): + with pytest.raises(TypeError, match="unexpected keyword argument 'mask_nodata'"): open_geotiff(path, mask_nodata=True) @@ -164,7 +164,7 @@ def test_default_name_sets_array_name(tmp_path): def test_name_removed(tmp_path): path = _int_sentinel_tiff(str(tmp_path / "t2961_name_alias.tif")) - with pytest.raises(TypeError, match="name"): + with pytest.raises(TypeError, match="unexpected keyword argument 'name'"): open_geotiff(path, name="elevation") @@ -175,7 +175,7 @@ def test_name_removed(tmp_path): def test_mask_and_scale_removed(tmp_path): """The 0.x ``mask_and_scale=`` alias is gone in 1.0; use ``unpack=``.""" path = _scale_offset_tiff(str(tmp_path / "t3071_ms_alias.tif")) - with pytest.raises(TypeError, match="mask_and_scale"): + with pytest.raises(TypeError, match="unexpected keyword argument 'mask_and_scale'"): open_geotiff(path, mask_and_scale=True) diff --git a/xrspatial/geotiff/tests/write/test_basic.py b/xrspatial/geotiff/tests/write/test_basic.py index 32c028b3d..a88b6ce8f 100644 --- a/xrspatial/geotiff/tests/write/test_basic.py +++ b/xrspatial/geotiff/tests/write/test_basic.py @@ -940,7 +940,7 @@ def test_write_vrt_vrt_path_kwarg_removed(tmp_path): """The old ``vrt_path=`` spelling is gone in 1.0; use ``path=``.""" src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') - with pytest.raises(TypeError, match='vrt_path'): + with pytest.raises(TypeError, match="unexpected keyword argument 'vrt_path'"): _build_vrt(vrt_path=out, source_files=[src]) @@ -1102,7 +1102,7 @@ def test_write_vrt_crs_wkt_removed(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() - with pytest.raises(TypeError, match='crs_wkt'): + with pytest.raises(TypeError, match="unexpected keyword argument 'crs_wkt'"): _build_vrt(vrt_path, [src], crs_wkt=wkt) out = _build_vrt(vrt_path, [src], crs=wkt) assert out == vrt_path From cedcc6b3aaee4a253a6e642b8c223db21e7138a0 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:28:20 -0500 Subject: [PATCH 3/4] Update signature and docstring pins that encoded the removed aliases (#3759) --- .../tests/parity/test_backend_matrix.py | 2 +- .../tests/read/test_unpack_noop_doc_3263.py | 2 +- xrspatial/geotiff/tests/test_polish.py | 2 +- .../geotiff/tests/unit/test_signatures.py | 55 ++++--------------- 4 files changed, 15 insertions(+), 46 deletions(-) diff --git a/xrspatial/geotiff/tests/parity/test_backend_matrix.py b/xrspatial/geotiff/tests/parity/test_backend_matrix.py index 3bdb52590..ca18f6762 100644 --- a/xrspatial/geotiff/tests/parity/test_backend_matrix.py +++ b/xrspatial/geotiff/tests/parity/test_backend_matrix.py @@ -465,7 +465,7 @@ def _build_miniswhite(dir_path: Path, target: Path) -> Path: expected_masked=False, source_type=_SRC_LOCAL_TIFF, builder=_build_int8_unmasked, - read_kwargs={"mask_nodata": False}, + read_kwargs={"masked": False}, ), _FixtureSpec( fix_id="cog-float32", diff --git a/xrspatial/geotiff/tests/read/test_unpack_noop_doc_3263.py b/xrspatial/geotiff/tests/read/test_unpack_noop_doc_3263.py index 2011dcf0f..0771a702b 100644 --- a/xrspatial/geotiff/tests/read/test_unpack_noop_doc_3263.py +++ b/xrspatial/geotiff/tests/read/test_unpack_noop_doc_3263.py @@ -37,7 +37,7 @@ def test_unpack_doc_does_not_claim_noop_3263(): # Scope to the ``unpack`` paragraph so an unrelated parameter # documenting a legitimate no-op case cannot fail this pin. start = doc.index("unpack : bool") - end = doc.index("mask_and_scale : bool", start) + end = doc.index("parse_coordinates : bool", start) unpack_doc = doc[start:end] assert "is a no-op" not in unpack_doc # The corrected wording names what actually happens. diff --git a/xrspatial/geotiff/tests/test_polish.py b/xrspatial/geotiff/tests/test_polish.py index d8cb18b8d..cbd460950 100644 --- a/xrspatial/geotiff/tests/test_polish.py +++ b/xrspatial/geotiff/tests/test_polish.py @@ -110,7 +110,7 @@ def test_docstring_lists_kwargs(self): # Defensive: the docstring is the contract here -- guard # against future regressions. assert 'relative' in _build_vrt.__doc__ - assert 'crs_wkt' in _build_vrt.__doc__ + assert 'crs' in _build_vrt.__doc__ assert 'nodata' in _build_vrt.__doc__ diff --git a/xrspatial/geotiff/tests/unit/test_signatures.py b/xrspatial/geotiff/tests/unit/test_signatures.py index 63be1539d..26cf62f9b 100644 --- a/xrspatial/geotiff/tests/unit/test_signatures.py +++ b/xrspatial/geotiff/tests/unit/test_signatures.py @@ -145,23 +145,10 @@ def test_write_geotiff_gpu_path_annotated(): def test_write_vrt_path_annotated(): """``_build_vrt(path, ...)`` is str-only (VRT writes are path-only by design; no file-like buffer support). The canonical name - is ``path`` (parity with ``to_geotiff`` / ``_write_geotiff_gpu``). - The annotation is plain ``str``: the default value is a private - sentinel (not ``None``) so the deprecation shim can distinguish - ``_build_vrt(path=None, ...)`` (rejected with TypeError) from a - caller who omitted ``path`` entirely (routed through the ``vrt_path`` - alias).""" + is ``path`` (parity with ``to_geotiff`` / ``_write_geotiff_gpu``).""" assert _annotation(_build_vrt, 'path') == 'str' -def test_write_vrt_vrt_path_annotated(): - """The deprecated ``vrt_path`` alias keeps the same ``str | None`` - annotation as ``path`` (str-only at the type level; ``None`` only - appears because the sentinel default lets the shim detect omission). - Pinned so a future re-rename does not silently widen the alias.""" - assert _annotation(_build_vrt, 'vrt_path') == 'str | None' - - # --- source: str or BinaryIO (open_geotiff is the public dispatch) --- @@ -229,12 +216,6 @@ def test_read_geotiff_gpu_on_gpu_failure_annotated(): assert _annotation(_read_geotiff_gpu, 'on_gpu_failure') == 'str' -def test_read_geotiff_gpu_deprecated_gpu_alias_annotated(): - """The deprecated ``gpu=`` alias on ``_read_geotiff_gpu`` carries the - same ``str`` annotation as the new ``on_gpu_failure`` kwarg.""" - assert _annotation(_read_geotiff_gpu, 'gpu') == 'str' - - # --- nodata: float | int | None on every writer entry point --- @@ -403,8 +384,8 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_runtime_noop(tmp_path): "bbox", "overview_level", "band", - # rioxarray ``open_rasterio`` name for the DataArray. Sits immediately - # before its deprecated ``name`` alias. + # rioxarray ``open_rasterio`` name for the DataArray on ``open_geotiff``; + # the internal readers spell the same kwarg ``name``. "default_name", "name", "chunks", @@ -431,17 +412,13 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_runtime_noop(tmp_path): "allow_experimental_codecs", "allow_internal_only_jpeg", "band_nodata", - # rioxarray ``open_rasterio`` masking flag (default False). Sits - # immediately before its deprecated ``mask_nodata`` alias. + # rioxarray ``open_rasterio`` masking flag (default False). "masked", - "mask_nodata", - # rioxarray-compatible read options. ``unpack`` (canonical) and its - # deprecated ``mask_and_scale`` alias, plus ``parse_coordinates``, are - # threaded into ``read_geotiff_dask``; ``unpack`` also reaches the GPU / - # dask+GPU backends (#3071). ``lock`` / ``cache`` are open_geotiff-only - # accept-and-warn shims. + # rioxarray-compatible read options. ``unpack`` and ``parse_coordinates`` + # are threaded into ``read_geotiff_dask``; ``unpack`` also reaches the + # GPU / dask+GPU backends (#3071). ``lock`` / ``cache`` are + # open_geotiff-only accept-and-warn shims. "unpack", - "mask_and_scale", "parse_coordinates", "lock", "cache", @@ -485,7 +462,8 @@ def _assert_canonical(fn, allowed_tail=()): def test_open_geotiff_defines_canonical_order(): """``open_geotiff``'s signature is the canonical reference.""" params = _kwonly_params(open_geotiff) - expected = list(_CANONICAL_ORDER) + # ``name`` is the internal readers' spelling of ``default_name``. + expected = [p for p in _CANONICAL_ORDER if p != "name"] assert params == expected, ( f"open_geotiff kw-only params {params!r} no longer match the " f"canonical order {expected!r}. Update both the function and the " @@ -495,18 +473,9 @@ def test_open_geotiff_defines_canonical_order(): def test_read_geotiff_gpu_matches_canonical_order(): """``_read_geotiff_gpu`` must list shared params in the canonical order.""" - # ``gpu`` here is the deprecated alias for ``on_gpu_failure`` (see - # ``_read_geotiff_gpu``'s docstring). It is not the boolean backend - # selector that lives on ``open_geotiff`` / ``_read_vrt``, so it sits - # at the tail rather than in its canonical-order slot. params = _kwonly_params(_read_geotiff_gpu) - # ``gpu`` is the deprecated alias, intentionally last. - assert params[-1] == "gpu", ( - f"_read_geotiff_gpu must keep the deprecated 'gpu' alias as the last " - f"kwarg; got {params!r}" - ) - # Drop the alias and run the canonical-subset check on the rest. - head = params[:-1] + assert "gpu" not in params, "the 0.x gpu= string alias was removed in 1.0" + head = params canonical_head = [p for p in _CANONICAL_ORDER if p in head] assert head == canonical_head, ( f"_read_geotiff_gpu kwarg order {head!r} does not match the canonical " From 5d6ca4d40e9ef964bab0d5236ee867e2881a33f2 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 6 Nov 2026 10:29:00 -0500 Subject: [PATCH 4/4] Keep the internal readers' kwarg spellings in the canonical-order pin (#3759) --- xrspatial/geotiff/tests/unit/test_signatures.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/xrspatial/geotiff/tests/unit/test_signatures.py b/xrspatial/geotiff/tests/unit/test_signatures.py index 26cf62f9b..d9b9b9cf8 100644 --- a/xrspatial/geotiff/tests/unit/test_signatures.py +++ b/xrspatial/geotiff/tests/unit/test_signatures.py @@ -412,13 +412,17 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_runtime_noop(tmp_path): "allow_experimental_codecs", "allow_internal_only_jpeg", "band_nodata", - # rioxarray ``open_rasterio`` masking flag (default False). + # rioxarray ``open_rasterio`` masking flag (default False) on + # ``open_geotiff``; the internal readers spell it ``mask_nodata``. "masked", + "mask_nodata", # rioxarray-compatible read options. ``unpack`` and ``parse_coordinates`` # are threaded into ``read_geotiff_dask``; ``unpack`` also reaches the - # GPU / dask+GPU backends (#3071). ``lock`` / ``cache`` are - # open_geotiff-only accept-and-warn shims. + # GPU / dask+GPU backends (#3071) and the internal readers spell it + # ``mask_and_scale``. ``lock`` / ``cache`` are open_geotiff-only + # accept-and-warn shims. "unpack", + "mask_and_scale", "parse_coordinates", "lock", "cache", @@ -462,8 +466,11 @@ def _assert_canonical(fn, allowed_tail=()): def test_open_geotiff_defines_canonical_order(): """``open_geotiff``'s signature is the canonical reference.""" params = _kwonly_params(open_geotiff) - # ``name`` is the internal readers' spelling of ``default_name``. - expected = [p for p in _CANONICAL_ORDER if p != "name"] + # ``name`` / ``mask_nodata`` / ``mask_and_scale`` are the internal + # readers' spellings of ``default_name`` / ``masked`` / ``unpack``; the + # public function only accepts the rioxarray names. + internal_only = {"name", "mask_nodata", "mask_and_scale"} + expected = [p for p in _CANONICAL_ORDER if p not in internal_only] assert params == expected, ( f"open_geotiff kw-only params {params!r} no longer match the " f"canonical order {expected!r}. Update both the function and the "