From c4ed333372717b5216bd0fc512e89277c930b498 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 12 May 2026 05:41:45 -0700 Subject: [PATCH 1/2] Annotate window/path/on_gpu_failure on public geotiff API (#1654) The api-consistency sweep on 2026-05-12 found the same parameter is annotated on some siblings but not on others across the public xrspatial.geotiff surface. Pin each annotation so type-checkers and IDEs validate user code consistently. - window: tuple | None on open_geotiff and read_vrt (read_geotiff_dask and read_geotiff_gpu already had it). - path: str | BinaryIO on to_geotiff and write_geotiff_gpu. write_vrt stays str-only because VRT writes are path-only by design. - on_gpu_failure: str on open_geotiff and read_geotiff_gpu. The deprecated gpu alias on read_geotiff_gpu carries the same str hint. Annotation-only change; no runtime behaviour, defaults, or kwarg renames. BinaryIO is imported under TYPE_CHECKING so the runtime import cost stays at zero with from __future__ import annotations. test_signature_annotations_1654.py pins each annotation to guard against future signature drift. Also updates the api-consistency sweep state CSV. --- .claude/sweep-api-consistency-state.csv | 3 +- xrspatial/geotiff/__init__.py | 23 ++- .../tests/test_signature_annotations_1654.py | 136 ++++++++++++++++++ 3 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 xrspatial/geotiff/tests/test_signature_annotations_1654.py diff --git a/.claude/sweep-api-consistency-state.csv b/.claude/sweep-api-consistency-state.csv index 0d21cfdd..75e332a3 100644 --- a/.claude/sweep-api-consistency-state.csv +++ b/.claude/sweep-api-consistency-state.csv @@ -1,4 +1,3 @@ module,last_inspected,issue,severity_max,categories_found,notes -geotiff,2026-05-11,1644,MEDIUM,3,"Filed write_geotiff_gpu compression docstring drift vs to_geotiff (MEDIUM Cat 3, #1644). Fix on deep-sweep-api-consistency-geotiff-2026-05-11-1778545740: sync the full 9-codec list into the docstring and note GPU vs CPU encode paths; regression test test_compression_docstring_1644.py pins the codec list and exercises each CPU-fallback codec end-to-end. Other potential drifts surveyed: write_vrt returns str while to_geotiff/write_geotiff_gpu return None (LOW, intentional backward-compat); write_vrt nodata typed float|None vs int-accepting siblings (LOW, PEP 484 int->float compat); kwarg-only ordering drift across read functions (LOW, no user impact). Prior issues 1631/1637/1615/1560/1541/1562 all CLOSED." -geotiff,2026-05-11,1652,MEDIUM,5,"Filed MEDIUM file-like cog=True drift #1652 (write_geotiff_gpu accepted BytesIO+cog=True; to_geotiff blocked it). Fixed in PR (TBD): mirror to_geotiff's gate on the explicit GPU writer; add regression tests in test_bytesio_source.py. Also filed #1651 (JPEG acceptance drift) but downgraded to LOW after #1647 confirmed write_geotiff_gpu(jpeg) is deliberate advanced-API; PR (TBD) carries the docstring clarification. Prior 1631/1644 noted in earlier rows (1644 open, fix in PR #1649). LOW: streaming_buffer_bytes default drift to_geotiff=256MB vs write_geotiff_gpu=None (no functional impact, explicit forwarding); to_geotiff data: annotation misses cupy.ndarray (accepted via auto-dispatch). cuda-validated." +geotiff,2026-05-12,1654,MEDIUM,3,"Filed type-annotation drift #1654 (MEDIUM Cat 3): public window/path/on_gpu_failure annotations missing on some siblings while others carry them. Fixed on deep-sweep-api-consistency-geotiff-2026-05-12 with PR (TBD): added tuple|None on open_geotiff/read_vrt window, str|BinaryIO on to_geotiff/write_geotiff_gpu path, str on open_geotiff/read_geotiff_gpu on_gpu_failure plus the deprecated gpu alias. Regression test test_signature_annotations_1654.py pins each annotation. Prior drifts surveyed: chunks default 512 vs None (intentional, read_geotiff_dask is dask-only); crs vs crs_wkt on write_vrt (VRT is WKT-only); write_vrt returns str (intentional backward-compat); nodata float|None vs unannotated (LOW, prior sweep). cuda-validated." reproject,2026-05-10,1570,HIGH,2;5,"Filed cross-module attrs['vertical_crs'] type collision (string vs EPSG int) vs xrspatial.geotiff. Fixed in PR (TBD): reproject now writes EPSG int and preserves friendly token under vertical_datum. MEDIUM kwarg-order drift (transform_precision vs chunk_size) and missing type hints vs geotiff documented but not fixed (cosmetic, kwarg-only)." diff --git a/xrspatial/geotiff/__init__.py b/xrspatial/geotiff/__init__.py index 41d89fa2..5e69a156 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -33,10 +33,14 @@ import math import warnings +from typing import TYPE_CHECKING import numpy as np import xarray as xr +if TYPE_CHECKING: + from typing import BinaryIO + from ._geotags import GeoTransform, RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT from ._reader import read_to_array from ._writer import write @@ -446,14 +450,16 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None break -def open_geotiff(source, *, dtype=None, window=None, +def open_geotiff(source, *, dtype=None, + window: tuple | None = None, overview_level: int | None = None, band: int | None = None, name: str | None = None, chunks: int | tuple | None = None, gpu: bool = False, max_pixels: int | None = None, - on_gpu_failure=_ON_GPU_FAILURE_SENTINEL) -> xr.DataArray: + on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, + ) -> xr.DataArray: """Read a GeoTIFF, COG, or VRT file into an xarray.DataArray. Automatically dispatches to the best backend: @@ -905,7 +911,8 @@ def _extract_rich_tags(attrs: dict) -> dict: } -def to_geotiff(data: xr.DataArray | np.ndarray, path, *, +def to_geotiff(data: xr.DataArray | np.ndarray, + path: str | BinaryIO, *, crs: int | str | None = None, nodata=None, compression: str = 'zstd', @@ -2178,8 +2185,9 @@ def read_geotiff_gpu(source: str, *, name: str | None = None, chunks: int | tuple | None = None, max_pixels: int | None = None, - on_gpu_failure=_ON_GPU_FAILURE_SENTINEL, - gpu=_GPU_DEPRECATED_SENTINEL) -> xr.DataArray: + on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, + gpu: str = _GPU_DEPRECATED_SENTINEL, + ) -> xr.DataArray: """Read a GeoTIFF with GPU-accelerated decompression via Numba CUDA. Decompresses all tiles in parallel on the GPU and returns a @@ -2714,7 +2722,7 @@ def _read_once(): def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, - path, *, + path: str | BinaryIO, *, crs: int | str | None = None, nodata=None, compression: str = 'zstd', @@ -3069,7 +3077,8 @@ def _gpu_compress_to_part(gpu_arr, w, h, spp): _write_bytes(file_bytes, path) -def read_vrt(source: str, *, dtype=None, window=None, +def read_vrt(source: str, *, dtype=None, + window: tuple | None = None, band: int | None = None, name: str | None = None, chunks: int | tuple | None = None, diff --git a/xrspatial/geotiff/tests/test_signature_annotations_1654.py b/xrspatial/geotiff/tests/test_signature_annotations_1654.py new file mode 100644 index 00000000..e183603d --- /dev/null +++ b/xrspatial/geotiff/tests/test_signature_annotations_1654.py @@ -0,0 +1,136 @@ +"""Regression test for #1654: public geotiff API parameter annotations. + +The api-consistency sweep on 2026-05-12 flagged a MEDIUM type-annotation +drift across the public ``xrspatial.geotiff`` surface. The same parameter +was annotated on some sibling functions but missing on others: + +* ``window``: annotated on ``read_geotiff_dask`` and ``read_geotiff_gpu`` + but missing on ``open_geotiff`` and ``read_vrt``. +* ``path``: annotated on ``write_vrt.vrt_path`` (str-only) but missing + on ``to_geotiff`` and ``write_geotiff_gpu`` (str or binary file-like). +* ``on_gpu_failure`` (and the deprecated ``gpu`` alias on + ``read_geotiff_gpu``): documented as ``{'auto', 'strict'}`` strings + but no annotation. The sentinel default did not preclude annotating + the user-visible value type. + +This module pins each annotation so future signature changes do not +silently drop them. +""" +from __future__ import annotations + +import inspect + +from xrspatial.geotiff import ( + open_geotiff, + read_geotiff_dask, + read_geotiff_gpu, + read_vrt, + to_geotiff, + write_geotiff_gpu, + write_vrt, +) + + +def _annotation(fn, param_name): + """Return the stringified annotation for ``fn``'s ``param_name``. + + ``from __future__ import annotations`` keeps annotations as strings + at runtime, so the comparison works against the source literal. + """ + sig = inspect.signature(fn) + p = sig.parameters[param_name] + assert p.annotation is not inspect.Parameter.empty, ( + f"{fn.__name__}({param_name}=...) is missing a type annotation" + ) + return str(p.annotation) + + +# --- window: 4-tuple (r0, c0, r1, c1) or None --- + + +def test_open_geotiff_window_annotated(): + assert _annotation(open_geotiff, 'window') == 'tuple | None' + + +def test_read_vrt_window_annotated(): + assert _annotation(read_vrt, 'window') == 'tuple | None' + + +def test_read_geotiff_dask_window_annotated(): + """Pre-existing annotation -- keep it pinned so it does not regress.""" + assert _annotation(read_geotiff_dask, 'window') == 'tuple | None' + + +def test_read_geotiff_gpu_window_annotated(): + """Pre-existing annotation -- keep it pinned so it does not regress.""" + assert _annotation(read_geotiff_gpu, 'window') == 'tuple | None' + + +# --- path: str or binary file-like (writer entry points) --- + + +def test_to_geotiff_path_annotated(): + """``to_geotiff(data, path, ...)`` ``path`` accepts str or BinaryIO.""" + ann = _annotation(to_geotiff, 'path') + assert 'str' in ann + assert 'BinaryIO' in ann + + +def test_write_geotiff_gpu_path_annotated(): + """``write_geotiff_gpu(data, path, ...)`` ``path`` mirrors ``to_geotiff``.""" + ann = _annotation(write_geotiff_gpu, 'path') + assert 'str' in ann + assert 'BinaryIO' in ann + + +def test_write_vrt_vrt_path_annotated(): + """``write_vrt(vrt_path, ...)`` stays str-only (VRT writes are + path-only by design; no file-like buffer support).""" + assert _annotation(write_vrt, 'vrt_path') == 'str' + + +# --- on_gpu_failure: 'auto' | 'strict' (GPU failure policy) --- + + +def test_open_geotiff_on_gpu_failure_annotated(): + assert _annotation(open_geotiff, 'on_gpu_failure') == 'str' + + +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' + + +# --- Smoke: the new annotations do not break runtime call semantics --- + + +def test_open_geotiff_window_and_failure_kwargs_runtime(): + """The annotated kwargs still accept the documented values at runtime.""" + import os + import tempfile + + import numpy as np + import xarray as xr + + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + da = xr.DataArray( + arr, dims=['y', 'x'], + coords={'y': np.arange(8.0, 0, -1), 'x': np.arange(8.0)}, + attrs={'crs': 4326, 'transform': (1.0, 0, 0.0, 0, -1.0, 8.0)}, + ) + + with tempfile.NamedTemporaryFile(suffix='.tif', delete=False) as f: + path = f.name + try: + to_geotiff(da, path) + # window is a 4-tuple; on_gpu_failure must not be passed on + # gpu=False, so just verify window kwarg roundtrip + r = open_geotiff(path, window=(0, 0, 4, 4)) + assert r.shape == (4, 4) + finally: + os.unlink(path) From 63740afb6c591cf0a5c28e937f18db6b289a9770 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Tue, 12 May 2026 06:21:01 -0700 Subject: [PATCH 2/2] Use tmp_path fixture for Windows-compatible cleanup (#1658) --- .../tests/test_signature_annotations_1654.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/xrspatial/geotiff/tests/test_signature_annotations_1654.py b/xrspatial/geotiff/tests/test_signature_annotations_1654.py index e183603d..4d292a6c 100644 --- a/xrspatial/geotiff/tests/test_signature_annotations_1654.py +++ b/xrspatial/geotiff/tests/test_signature_annotations_1654.py @@ -109,11 +109,12 @@ def test_read_geotiff_gpu_deprecated_gpu_alias_annotated(): # --- Smoke: the new annotations do not break runtime call semantics --- -def test_open_geotiff_window_and_failure_kwargs_runtime(): - """The annotated kwargs still accept the documented values at runtime.""" - import os - import tempfile - +def test_open_geotiff_window_kwarg_runtime(tmp_path): + """The annotated ``window`` kwarg still accepts a 4-tuple and returns + the requested sub-window. The test does not exercise ``on_gpu_failure`` + because the runtime semantics are GPU-only; the annotation itself is + pinned by ``test_open_geotiff_on_gpu_failure_annotated``. + """ import numpy as np import xarray as xr @@ -124,13 +125,7 @@ def test_open_geotiff_window_and_failure_kwargs_runtime(): attrs={'crs': 4326, 'transform': (1.0, 0, 0.0, 0, -1.0, 8.0)}, ) - with tempfile.NamedTemporaryFile(suffix='.tif', delete=False) as f: - path = f.name - try: - to_geotiff(da, path) - # window is a 4-tuple; on_gpu_failure must not be passed on - # gpu=False, so just verify window kwarg roundtrip - r = open_geotiff(path, window=(0, 0, 4, 4)) - assert r.shape == (4, 4) - finally: - os.unlink(path) + path = str(tmp_path / 'window_kwarg.tif') + to_geotiff(da, path) + r = open_geotiff(path, window=(0, 0, 4, 4)) + assert r.shape == (4, 4)