Skip to content

Commit 3940c18

Browse files
committed
Replace silent except-return-None with typed warnings + strict mode (#1662)
Each broad `except Exception: return None` site flagged in #1662 now emits a GeoTIFFFallbackWarning carrying the original exception type and message. The new XRSPATIAL_GEOTIFF_STRICT=1 env var (read by _geotiff_strict_mode()) re-raises instead of warning so CI can fail on silent GPU or VRT fallbacks. Sites covered: - _wkt_to_epsg, _epsg_to_wkt: pyproj-missing vs broken-input - _vrt.py source read skip: surface missing-tile holes - _gds_read_tiles, _try_nvcomp_batch_decompress (kvikio + ctypes), _try_nvjpeg_batch_decode, _nvjpeg_batch_encode, _try_nvcomp_from_device_bufs, _nvcomp_batch_compress, _try_nvjpeg2k_batch_decode, _nvjpeg2k_batch_encode - read_geotiff_gpu(on_gpu_failure='auto') stages also honor the env var
1 parent f0a09c0 commit 3940c18

4 files changed

Lines changed: 114 additions & 18 deletions

File tree

xrspatial/geotiff/__init__.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from __future__ import annotations
3333

3434
import math
35+
import os
3536
import warnings
3637

3738
import numpy as np
@@ -75,17 +76,50 @@
7576
_BAND_DIM_NAMES = ('band', 'bands', 'channel')
7677

7778

79+
class GeoTIFFFallbackWarning(UserWarning):
80+
"""Warning emitted when a geotiff helper falls back to a slower path.
81+
82+
Raised in the same call sites that would silently return ``None`` under
83+
the historic ``except Exception: return None`` pattern. See issue #1662
84+
for the audit and the ``XRSPATIAL_GEOTIFF_STRICT=1`` env var that
85+
promotes these warnings to exceptions.
86+
"""
87+
88+
89+
def _geotiff_strict_mode() -> bool:
90+
"""Return True when ``XRSPATIAL_GEOTIFF_STRICT`` is set to a truthy value.
91+
92+
Strict mode promotes the silent fallbacks audited in issue #1662 into
93+
raised exceptions. Useful in CI to catch GPU-path or VRT regressions
94+
that would otherwise hide behind a CPU fallback or a missing tile.
95+
"""
96+
return os.environ.get(
97+
'XRSPATIAL_GEOTIFF_STRICT', '').lower() in ('1', 'true', 'yes')
98+
99+
78100
def _wkt_to_epsg(wkt_or_proj: str) -> int | None:
79101
"""Try to extract an EPSG code from a WKT or PROJ string.
80102
81103
Returns None if pyproj is not installed or the string can't be parsed.
104+
105+
Under ``XRSPATIAL_GEOTIFF_STRICT=1`` the underlying exception is
106+
re-raised instead of being swallowed. In the default mode a
107+
``GeoTIFFFallbackWarning`` is emitted so callers can tell
108+
pyproj-missing from pyproj-broken-input.
82109
"""
83110
try:
84111
from pyproj import CRS
85112
crs = CRS.from_user_input(wkt_or_proj)
86113
epsg = crs.to_epsg()
87114
return epsg
88-
except Exception:
115+
except Exception as e:
116+
if _geotiff_strict_mode():
117+
raise
118+
warnings.warn(
119+
f"_wkt_to_epsg failed ({type(e).__name__}: {e}); returning None.",
120+
GeoTIFFFallbackWarning,
121+
stacklevel=2,
122+
)
89123
return None
90124

91125

@@ -1968,7 +2002,7 @@ def _gpu_decode_single_band_tiles(
19682002
byte_order=byte_order,
19692003
)
19702004
except Exception as e:
1971-
if gpu == 'strict':
2005+
if gpu == 'strict' or _geotiff_strict_mode():
19722006
raise
19732007
warnings.warn(
19742008
f"read_geotiff_gpu: GPU decode failed "
@@ -1992,7 +2026,7 @@ def _gpu_decode_single_band_tiles(
19922026
byte_order=byte_order,
19932027
)
19942028
except Exception as e:
1995-
if gpu == 'strict':
2029+
if gpu == 'strict' or _geotiff_strict_mode():
19962030
raise
19972031
warnings.warn(
19982032
f"read_geotiff_gpu: GPU decode failed "
@@ -2588,7 +2622,7 @@ def _read_once():
25882622
masked_fill=masked_fill,
25892623
)
25902624
except Exception as e:
2591-
if gpu == 'strict':
2625+
if gpu == 'strict' or _geotiff_strict_mode():
25922626
raise
25932627
warnings.warn(
25942628
f"read_geotiff_gpu: GPU decode failed "
@@ -2620,7 +2654,7 @@ def _read_once():
26202654
masked_fill=masked_fill,
26212655
)
26222656
except Exception as e:
2623-
if gpu == 'strict':
2657+
if gpu == 'strict' or _geotiff_strict_mode():
26242658
raise
26252659
warnings.warn(
26262660
f"read_geotiff_gpu: GPU decode failed "

xrspatial/geotiff/_geotags.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,24 @@ def _epsg_to_wkt(epsg: int) -> str | None:
235235
"""Resolve an EPSG code to a WKT string using pyproj.
236236
237237
Returns None if pyproj is not installed or the code is unknown.
238+
239+
Under ``XRSPATIAL_GEOTIFF_STRICT=1`` the underlying exception is
240+
re-raised instead of being swallowed. See issue #1662.
238241
"""
239242
try:
240243
from pyproj import CRS
241244
return CRS.from_epsg(epsg).to_wkt()
242-
except Exception:
245+
except Exception as e:
246+
import warnings
247+
from . import _geotiff_strict_mode, GeoTIFFFallbackWarning
248+
if _geotiff_strict_mode():
249+
raise
250+
warnings.warn(
251+
f"_epsg_to_wkt({epsg!r}) failed "
252+
f"({type(e).__name__}: {e}); returning None.",
253+
GeoTIFFFallbackWarning,
254+
stacklevel=2,
255+
)
243256
return None
244257

245258

xrspatial/geotiff/_gpu_decode.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,31 @@
77
from __future__ import annotations
88

99
import math
10+
import warnings
1011

1112
import numpy as np
1213
from numba import cuda
1314

15+
16+
def _warn_or_raise_gpu_fallback(stage: str, exc: BaseException) -> None:
17+
"""Report a GPU helper falling back to None (issue #1662).
18+
19+
Under ``XRSPATIAL_GEOTIFF_STRICT=1`` the original exception is
20+
re-raised so CI catches silent fast-path regressions. In the default
21+
mode a ``GeoTIFFFallbackWarning`` is emitted with the exception type
22+
and message; the caller still receives ``None`` and chooses its own
23+
next step (typically another decoder or CPU fallback).
24+
"""
25+
from . import _geotiff_strict_mode, GeoTIFFFallbackWarning
26+
if _geotiff_strict_mode():
27+
raise exc
28+
warnings.warn(
29+
f"{stage} fell back to None "
30+
f"({type(exc).__name__}: {exc}).",
31+
GeoTIFFFallbackWarning,
32+
stacklevel=3,
33+
)
34+
1435
#: Fraction of free GPU memory we're willing to allocate in a single call.
1536
#: Above this, raise MemoryError up-front so the caller gets an actionable
1637
#: error rather than a CUDA OOM deep inside the kernel launch.
@@ -941,14 +962,17 @@ def _try_kvikio_read_tiles(file_path, tile_offsets, tile_byte_counts, tile_bytes
941962
d_tiles.append(buf)
942963
cupy.cuda.Device().synchronize()
943964
return d_tiles
944-
except Exception:
945-
# GDS not available, version mismatch, or CUDA error
946-
# Reset CUDA error state if possible
965+
except Exception as e:
966+
# GDS not available, version mismatch, or CUDA error.
967+
# Reset CUDA error state if possible (the inner pass stays broad
968+
# because a failed synchronize() during error recovery has no
969+
# better recovery path; see issue #1662).
947970
try:
948971
import cupy
949972
cupy.cuda.Device().synchronize()
950973
except Exception:
951974
pass
975+
_warn_or_raise_gpu_fallback("_gds_read_tiles", e)
952976
return None
953977

954978

@@ -1051,7 +1075,9 @@ def _try_nvcomp_batch_decompress(compressed_tiles, tile_bytes, compression):
10511075
]
10521076
d_decompressed = manager.decompress(d_compressed)
10531077
return cupy.concatenate([d.ravel() for d in d_decompressed])
1054-
except Exception:
1078+
except Exception as e:
1079+
_warn_or_raise_gpu_fallback(
1080+
"_try_nvcomp_batch_decompress (kvikio DeflateManager)", e)
10551081
return None
10561082

10571083
# Direct ctypes nvCOMP C API
@@ -1170,7 +1196,9 @@ class _NvcompDeflateDecompOpts(ctypes.Structure):
11701196

11711197
return d_decomp
11721198

1173-
except Exception:
1199+
except Exception as e:
1200+
_warn_or_raise_gpu_fallback(
1201+
"_try_nvcomp_batch_decompress (ctypes nvCOMP C API)", e)
11741202
return None
11751203

11761204

@@ -1347,7 +1375,8 @@ class _NvjpegImage(ctypes.Structure):
13471375
if destroy_fn is not None:
13481376
destroy_fn(nvjpeg_handle)
13491377

1350-
except Exception:
1378+
except Exception as e:
1379+
_warn_or_raise_gpu_fallback("_try_nvjpeg_batch_decode", e)
13511380
return None
13521381

13531382

@@ -1491,7 +1520,8 @@ class _NvjpegImage(ctypes.Structure):
14911520
if destroy_fn is not None:
14921521
destroy_fn(nvjpeg_handle)
14931522

1494-
except Exception:
1523+
except Exception as e:
1524+
_warn_or_raise_gpu_fallback("_nvjpeg_batch_encode", e)
14951525
return None
14961526

14971527

@@ -1622,7 +1652,8 @@ class _NvcompDecompOpts(ctypes.Structure):
16221652
return None
16231653

16241654
return cupy.concatenate(d_decomp_bufs)
1625-
except Exception:
1655+
except Exception as e:
1656+
_warn_or_raise_gpu_fallback("_try_nvcomp_from_device_bufs", e)
16261657
return None
16271658

16281659

@@ -2408,7 +2439,8 @@ class _DeflateCompOpts(ctypes.Structure):
24082439

24092440
return result
24102441

2411-
except Exception:
2442+
except Exception as e:
2443+
_warn_or_raise_gpu_fallback("_nvcomp_batch_compress", e)
24122444
return None
24132445

24142446

@@ -2597,7 +2629,8 @@ class _NvJpeg2kImage(ctypes.Structure):
25972629

25982630
return d_all_tiles
25992631

2600-
except Exception:
2632+
except Exception as e:
2633+
_warn_or_raise_gpu_fallback("_try_nvjpeg2k_batch_decode", e)
26012634
return None
26022635

26032636

@@ -2742,7 +2775,8 @@ class _CompInfo(ctypes.Structure):
27422775

27432776
return result
27442777

2745-
except Exception:
2778+
except Exception as e:
2779+
_warn_or_raise_gpu_fallback("_nvjpeg2k_batch_encode", e)
27462780
return None
27472781

27482782

xrspatial/geotiff/_vrt.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,22 @@ def read_vrt(vrt_path: str, *, window=None,
356356
window=(src_r0, src_c0, src_r1, src_c1),
357357
band=src.band - 1, # convert 1-based to 0-based
358358
)
359-
except Exception:
359+
except Exception as e:
360+
# Under XRSPATIAL_GEOTIFF_STRICT=1, surface the read failure
361+
# so partial mosaics are caught in CI. Default mode warns
362+
# once per missing source then continues, preserving the
363+
# historical behaviour. See issue #1662.
364+
import warnings
365+
from . import _geotiff_strict_mode, GeoTIFFFallbackWarning
366+
if _geotiff_strict_mode():
367+
raise
368+
warnings.warn(
369+
f"VRT source {src.filename!r} could not be read "
370+
f"({type(e).__name__}: {e}); skipping. The output "
371+
f"mosaic will have a hole at this tile.",
372+
GeoTIFFFallbackWarning,
373+
stacklevel=2,
374+
)
360375
continue # skip missing/unreadable sources
361376

362377
# Handle source nodata. Cast the sentinel to the *source*

0 commit comments

Comments
 (0)