Skip to content

Commit 672ebfc

Browse files
authored
Rename read_geotiff_gpu gpu kwarg to on_gpu_failure (#1560) (#1590)
* Rename read_geotiff_gpu gpu kwarg to on_gpu_failure (#1560) The kwarg shared a name with the boolean gpu= on open_geotiff / to_geotiff / read_vrt but carried a different value space ('auto' or 'strict'). Calling read_geotiff_gpu(path, gpu=True) -- the natural mental model after using open_geotiff(path, gpu=True) -- raised "gpu must be 'auto' or 'strict', got True", obscuring the rename and forcing readers to dig through the source to learn the correct values. Rename to on_gpu_failure and keep gpu= as a deprecation shim: - on_gpu_failure: canonical name, same {'auto', 'strict'} value space. - gpu: still accepted; emits DeprecationWarning that points to the new name and explains why the rename happened. - Passing both raises TypeError to avoid silently picking one. - Validation error message names on_gpu_failure so the rename is discoverable from the traceback. Existing test_gpu_strict_fallback_1516.py keeps using gpu='strict' via the shim (with DeprecationWarning, as expected). One assertion that hardcoded the old error string is updated to the new wording. Module docstring updated to reference on_gpu_failure='strict' as the example. * Detect both-kwargs via sentinel in read_geotiff_gpu Copilot caught a hole in the both-supplied check: the old guard ``if on_gpu_failure != 'auto'`` only fired when the new kwarg was non-default, so calls like ``on_gpu_failure='auto', gpu='strict'`` (or ``on_gpu_failure='auto', gpu='auto'``) silently accepted the deprecated value, contradicting the documented contract. Use a sentinel for both kwargs so the both-supplied case is rejected regardless of values. Add parametrized regression tests for the three previously-silent value combinations. Also widen the accepted-exceptions tuple in test_gpu_alias_accepts_old_values_without_validation_error to include ImportError, so it stays meaningful in CPU-only CI (where ``import cupy`` raises after validation passes).
1 parent afb2e24 commit 672ebfc

3 files changed

Lines changed: 176 additions & 9 deletions

File tree

xrspatial/geotiff/__init__.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
read_geotiff_gpu(source, ...)
1212
GPU-only read returning a CuPy-backed DataArray. ``open_geotiff(...,
1313
gpu=True)`` calls this internally; use the explicit name when you want
14-
the strict-mode failure semantics (``gpu='strict'``) or want to bypass
15-
auto-dispatch.
14+
the strict-mode failure semantics (``on_gpu_failure='strict'``) or want
15+
to bypass auto-dispatch.
1616
read_geotiff_dask(source, ...)
1717
Dask-only read returning a windowed lazy DataArray. ``open_geotiff(...,
1818
chunks=N)`` calls this internally.
@@ -660,6 +660,16 @@ def _apply_nodata_mask_gpu(arr_gpu, nodata):
660660
)
661661

662662

663+
# Sentinels distinguishing "user passed this kwarg explicitly" from "user
664+
# passed nothing". A plain default of None would not work because None is
665+
# itself a value a caller could supply. ``read_geotiff_gpu`` needs both
666+
# sentinels so it can tell whether the deprecated ``gpu=`` and the new
667+
# ``on_gpu_failure=`` were *each* supplied, and refuse the ambiguous
668+
# both-supplied case regardless of which values were chosen.
669+
_GPU_DEPRECATED_SENTINEL = object()
670+
_ON_GPU_FAILURE_SENTINEL = object()
671+
672+
663673
# TIFF type ids needed when synthesizing extra_tags entries from attrs.
664674
_TIFF_BYTE = 1
665675
_TIFF_ASCII = 2
@@ -1813,7 +1823,8 @@ def read_geotiff_gpu(source: str, *,
18131823
name: str | None = None,
18141824
chunks: int | tuple | None = None,
18151825
max_pixels: int | None = None,
1816-
gpu: str = 'auto') -> xr.DataArray:
1826+
on_gpu_failure=_ON_GPU_FAILURE_SENTINEL,
1827+
gpu=_GPU_DEPRECATED_SENTINEL) -> xr.DataArray:
18171828
"""Read a GeoTIFF with GPU-accelerated decompression via Numba CUDA.
18181829
18191830
Decompresses all tiles in parallel on the GPU and returns a
@@ -1839,7 +1850,7 @@ def read_geotiff_gpu(source: str, *,
18391850
max_pixels : int or None
18401851
Maximum allowed pixel count (width * height * samples). None
18411852
uses the default (~1 billion).
1842-
gpu : {'auto', 'strict'}, default 'auto'
1853+
on_gpu_failure : {'auto', 'strict'}, default 'auto'
18431854
Behaviour when any GPU decode stage raises an exception.
18441855
18451856
The GPU pipeline has two stages: first ``gpu_decode_tiles_from_file``
@@ -1859,18 +1870,47 @@ def read_geotiff_gpu(source: str, *,
18591870
GPU fast path.
18601871
18611872
Stripped layouts and sparse-tile files route directly to the CPU
1862-
reader before either GPU decode stage runs, so the ``gpu`` kwarg
1863-
does not affect them. A failure inside the subsequent
1873+
reader before either GPU decode stage runs, so the ``on_gpu_failure``
1874+
kwarg does not affect them. A failure inside the subsequent
18641875
``cupy.asarray(...)`` upload propagates unchanged in both modes.
1876+
gpu : str, optional
1877+
Deprecated alias for ``on_gpu_failure``. Emits ``DeprecationWarning``
1878+
when used. Passing both ``gpu`` and ``on_gpu_failure`` raises
1879+
``TypeError``. The old name shipped with values ``'auto'`` /
1880+
``'strict'`` and was easy to confuse with the boolean ``gpu=``
1881+
kwarg on ``open_geotiff`` / ``to_geotiff`` / ``read_vrt``.
18651882
18661883
Returns
18671884
-------
18681885
xr.DataArray
18691886
CuPy-backed DataArray on GPU device.
18701887
"""
1888+
new_passed = on_gpu_failure is not _ON_GPU_FAILURE_SENTINEL
1889+
old_passed = gpu is not _GPU_DEPRECATED_SENTINEL
1890+
if new_passed and old_passed:
1891+
# Both supplied is ambiguous regardless of which values were
1892+
# chosen (including the matching ``on_gpu_failure='auto',
1893+
# gpu='auto'`` pair). Refuse rather than silently picking one.
1894+
raise TypeError(
1895+
"read_geotiff_gpu: pass either 'on_gpu_failure' or the "
1896+
"deprecated 'gpu' alias, not both.")
1897+
if old_passed:
1898+
warnings.warn(
1899+
"read_geotiff_gpu(..., gpu=...) is deprecated; use "
1900+
"on_gpu_failure=... instead. The kwarg was renamed because "
1901+
"'gpu' on open_geotiff/to_geotiff/read_vrt is a bool that "
1902+
"selects the GPU backend, while here it selects the failure "
1903+
"policy when the GPU path raises.",
1904+
DeprecationWarning,
1905+
stacklevel=2,
1906+
)
1907+
on_gpu_failure = gpu
1908+
elif not new_passed:
1909+
on_gpu_failure = 'auto'
1910+
gpu = on_gpu_failure
18711911
if gpu not in ('auto', 'strict'):
18721912
raise ValueError(
1873-
f"gpu must be 'auto' or 'strict', got {gpu!r}")
1913+
f"on_gpu_failure must be 'auto' or 'strict', got {gpu!r}")
18741914
try:
18751915
import cupy
18761916
except ImportError:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Regression tests for issue #1560.
2+
3+
``read_geotiff_gpu`` previously took a ``gpu={'auto','strict'}`` kwarg
4+
that controlled GPU-failure policy, sharing a name with the boolean
5+
``gpu=`` kwarg on ``open_geotiff`` / ``to_geotiff`` / ``read_vrt``.
6+
Calling ``read_geotiff_gpu(path, gpu=True)`` -- the mental model after
7+
using ``open_geotiff(path, gpu=True)`` -- raised the unhelpful
8+
``ValueError: gpu must be 'auto' or 'strict', got True``.
9+
10+
The fix renames the kwarg to ``on_gpu_failure`` and keeps ``gpu=`` as a
11+
deprecation shim:
12+
13+
* ``on_gpu_failure`` alone behaves like the old ``gpu`` kwarg.
14+
* ``gpu`` alone still works, but emits ``DeprecationWarning``.
15+
* Passing both raises ``TypeError``.
16+
17+
These tests exercise the validation path only, which fires before the
18+
``cupy`` import inside ``read_geotiff_gpu``. No GPU runtime needed.
19+
"""
20+
from __future__ import annotations
21+
22+
import warnings
23+
24+
import pytest
25+
26+
27+
def test_on_gpu_failure_invalid_value_raises_value_error():
28+
"""Bad ``on_gpu_failure`` value still raises ``ValueError``."""
29+
from xrspatial.geotiff import read_geotiff_gpu
30+
31+
with pytest.raises(ValueError, match="on_gpu_failure must be"):
32+
read_geotiff_gpu("/nonexistent.tif", on_gpu_failure='loose')
33+
34+
35+
def test_gpu_alias_emits_deprecation_warning():
36+
"""Old ``gpu=`` kwarg still routes through, with a DeprecationWarning."""
37+
from xrspatial.geotiff import read_geotiff_gpu
38+
39+
with warnings.catch_warnings(record=True) as records:
40+
warnings.simplefilter("always")
41+
# Pass an invalid sentinel so we don't have to mock the full GPU
42+
# pipeline; ValueError fires after the deprecation handler runs.
43+
with pytest.raises(ValueError, match="on_gpu_failure must be"):
44+
read_geotiff_gpu("/nonexistent.tif", gpu='loose')
45+
46+
deprecations = [r for r in records if issubclass(r.category, DeprecationWarning)]
47+
assert deprecations, "expected DeprecationWarning when gpu= is used"
48+
assert "on_gpu_failure" in str(deprecations[0].message)
49+
50+
51+
def test_gpu_alias_accepts_old_values_without_validation_error():
52+
"""``gpu='strict'`` was the legacy spelling; should still validate."""
53+
from xrspatial.geotiff import read_geotiff_gpu
54+
55+
with warnings.catch_warnings():
56+
# Suppress the deprecation noise; we only care that the value
57+
# passes validation and the call proceeds past the value check.
58+
# In CPU-only CI the next step is ``import cupy`` which raises
59+
# ``ImportError`` (cupy is an optional extra); on a GPU host it
60+
# gets to the file-read stage and raises ``FileNotFoundError``.
61+
# Either is fine: both mean validation passed.
62+
warnings.simplefilter("ignore", DeprecationWarning)
63+
with pytest.raises(
64+
(FileNotFoundError, OSError, ValueError, ImportError)
65+
) as exc_info:
66+
read_geotiff_gpu("/nonexistent.tif", gpu='strict')
67+
68+
# The validation ValueError carries our exact message; a generic
69+
# file-read or cupy-import failure is fine because it means
70+
# validation passed.
71+
if isinstance(exc_info.value, ValueError):
72+
assert "on_gpu_failure must be" not in str(exc_info.value)
73+
74+
75+
def test_passing_both_raises_type_error():
76+
"""Mixing the new and deprecated names is ambiguous; refuse."""
77+
from xrspatial.geotiff import read_geotiff_gpu
78+
79+
with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"):
80+
read_geotiff_gpu(
81+
"/nonexistent.tif",
82+
on_gpu_failure='strict',
83+
gpu='auto',
84+
)
85+
86+
87+
@pytest.mark.parametrize("on_gpu_failure_val,gpu_val", [
88+
('auto', 'strict'),
89+
('auto', 'auto'),
90+
('strict', 'strict'),
91+
])
92+
def test_passing_both_raises_regardless_of_values(on_gpu_failure_val, gpu_val):
93+
"""Both-supplied is rejected even when ``on_gpu_failure='auto'``.
94+
95+
A sentinel-based detection (rather than ``!= 'auto'``) catches the
96+
case where the caller passes the default value explicitly alongside
97+
the deprecated alias.
98+
"""
99+
from xrspatial.geotiff import read_geotiff_gpu
100+
101+
with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"):
102+
read_geotiff_gpu(
103+
"/nonexistent.tif",
104+
on_gpu_failure=on_gpu_failure_val,
105+
gpu=gpu_val,
106+
)
107+
108+
109+
def test_gpu_alias_bool_no_longer_misleading_value_error():
110+
"""Calling with ``gpu=True`` -- the documented bool from the dispatchers --
111+
used to raise ``ValueError: gpu must be 'auto' or 'strict', got True``.
112+
The new error explicitly names ``on_gpu_failure`` so the rename is
113+
discoverable from the traceback.
114+
"""
115+
from xrspatial.geotiff import read_geotiff_gpu
116+
117+
with warnings.catch_warnings():
118+
warnings.simplefilter("ignore", DeprecationWarning)
119+
with pytest.raises(ValueError, match="on_gpu_failure must be"):
120+
read_geotiff_gpu("/nonexistent.tif", gpu=True)

xrspatial/geotiff/tests/test_gpu_strict_fallback_1516.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import importlib
2626
import sys
2727
import types
28+
import warnings
2829

2930
import numpy as np
3031
import pytest
@@ -260,8 +261,14 @@ def test_invalid_gpu_kwarg_rejected(tiled_tiff_path):
260261

261262
path, _ = tiled_tiff_path
262263

263-
with pytest.raises(ValueError, match="gpu must be 'auto' or 'strict'"):
264-
read_geotiff_gpu(path, gpu='loose')
264+
# The kwarg was renamed to ``on_gpu_failure`` in #1560; the
265+
# validation error now names the new kwarg even when callers
266+
# supply the deprecated ``gpu=`` alias.
267+
with pytest.raises(ValueError,
268+
match="on_gpu_failure must be 'auto' or 'strict'"):
269+
with warnings.catch_warnings():
270+
warnings.simplefilter("ignore", DeprecationWarning)
271+
read_geotiff_gpu(path, gpu='loose')
265272
finally:
266273
if inserted_stub:
267274
_restore_cupy()

0 commit comments

Comments
 (0)