Skip to content

Commit cde7750

Browse files
authored
Annotate window / path / on_gpu_failure on public geotiff API (#1654) (#1658)
* 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. * Use tmp_path fixture for Windows-compatible cleanup (#1658)
1 parent 181bce7 commit cde7750

3 files changed

Lines changed: 148 additions & 9 deletions

File tree

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
module,last_inspected,issue,severity_max,categories_found,notes
2-
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."
3-
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."
2+
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."
43
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)."

xrspatial/geotiff/__init__.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@
3333

3434
import math
3535
import warnings
36+
from typing import TYPE_CHECKING
3637

3738
import numpy as np
3839
import xarray as xr
3940

41+
if TYPE_CHECKING:
42+
from typing import BinaryIO
43+
4044
from ._geotags import GeoTransform, RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT
4145
from ._reader import read_to_array
4246
from ._writer import write
@@ -446,14 +450,16 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None
446450
break
447451

448452

449-
def open_geotiff(source, *, dtype=None, window=None,
453+
def open_geotiff(source, *, dtype=None,
454+
window: tuple | None = None,
450455
overview_level: int | None = None,
451456
band: int | None = None,
452457
name: str | None = None,
453458
chunks: int | tuple | None = None,
454459
gpu: bool = False,
455460
max_pixels: int | None = None,
456-
on_gpu_failure=_ON_GPU_FAILURE_SENTINEL) -> xr.DataArray:
461+
on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL,
462+
) -> xr.DataArray:
457463
"""Read a GeoTIFF, COG, or VRT file into an xarray.DataArray.
458464
459465
Automatically dispatches to the best backend:
@@ -905,7 +911,8 @@ def _extract_rich_tags(attrs: dict) -> dict:
905911
}
906912

907913

908-
def to_geotiff(data: xr.DataArray | np.ndarray, path, *,
914+
def to_geotiff(data: xr.DataArray | np.ndarray,
915+
path: str | BinaryIO, *,
909916
crs: int | str | None = None,
910917
nodata=None,
911918
compression: str = 'zstd',
@@ -2178,8 +2185,9 @@ def read_geotiff_gpu(source: str, *,
21782185
name: str | None = None,
21792186
chunks: int | tuple | None = None,
21802187
max_pixels: int | None = None,
2181-
on_gpu_failure=_ON_GPU_FAILURE_SENTINEL,
2182-
gpu=_GPU_DEPRECATED_SENTINEL) -> xr.DataArray:
2188+
on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL,
2189+
gpu: str = _GPU_DEPRECATED_SENTINEL,
2190+
) -> xr.DataArray:
21832191
"""Read a GeoTIFF with GPU-accelerated decompression via Numba CUDA.
21842192
21852193
Decompresses all tiles in parallel on the GPU and returns a
@@ -2714,7 +2722,7 @@ def _read_once():
27142722

27152723

27162724
def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray,
2717-
path, *,
2725+
path: str | BinaryIO, *,
27182726
crs: int | str | None = None,
27192727
nodata=None,
27202728
compression: str = 'zstd',
@@ -3069,7 +3077,8 @@ def _gpu_compress_to_part(gpu_arr, w, h, spp):
30693077
_write_bytes(file_bytes, path)
30703078

30713079

3072-
def read_vrt(source: str, *, dtype=None, window=None,
3080+
def read_vrt(source: str, *, dtype=None,
3081+
window: tuple | None = None,
30733082
band: int | None = None,
30743083
name: str | None = None,
30753084
chunks: int | tuple | None = None,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Regression test for #1654: public geotiff API parameter annotations.
2+
3+
The api-consistency sweep on 2026-05-12 flagged a MEDIUM type-annotation
4+
drift across the public ``xrspatial.geotiff`` surface. The same parameter
5+
was annotated on some sibling functions but missing on others:
6+
7+
* ``window``: annotated on ``read_geotiff_dask`` and ``read_geotiff_gpu``
8+
but missing on ``open_geotiff`` and ``read_vrt``.
9+
* ``path``: annotated on ``write_vrt.vrt_path`` (str-only) but missing
10+
on ``to_geotiff`` and ``write_geotiff_gpu`` (str or binary file-like).
11+
* ``on_gpu_failure`` (and the deprecated ``gpu`` alias on
12+
``read_geotiff_gpu``): documented as ``{'auto', 'strict'}`` strings
13+
but no annotation. The sentinel default did not preclude annotating
14+
the user-visible value type.
15+
16+
This module pins each annotation so future signature changes do not
17+
silently drop them.
18+
"""
19+
from __future__ import annotations
20+
21+
import inspect
22+
23+
from xrspatial.geotiff import (
24+
open_geotiff,
25+
read_geotiff_dask,
26+
read_geotiff_gpu,
27+
read_vrt,
28+
to_geotiff,
29+
write_geotiff_gpu,
30+
write_vrt,
31+
)
32+
33+
34+
def _annotation(fn, param_name):
35+
"""Return the stringified annotation for ``fn``'s ``param_name``.
36+
37+
``from __future__ import annotations`` keeps annotations as strings
38+
at runtime, so the comparison works against the source literal.
39+
"""
40+
sig = inspect.signature(fn)
41+
p = sig.parameters[param_name]
42+
assert p.annotation is not inspect.Parameter.empty, (
43+
f"{fn.__name__}({param_name}=...) is missing a type annotation"
44+
)
45+
return str(p.annotation)
46+
47+
48+
# --- window: 4-tuple (r0, c0, r1, c1) or None ---
49+
50+
51+
def test_open_geotiff_window_annotated():
52+
assert _annotation(open_geotiff, 'window') == 'tuple | None'
53+
54+
55+
def test_read_vrt_window_annotated():
56+
assert _annotation(read_vrt, 'window') == 'tuple | None'
57+
58+
59+
def test_read_geotiff_dask_window_annotated():
60+
"""Pre-existing annotation -- keep it pinned so it does not regress."""
61+
assert _annotation(read_geotiff_dask, 'window') == 'tuple | None'
62+
63+
64+
def test_read_geotiff_gpu_window_annotated():
65+
"""Pre-existing annotation -- keep it pinned so it does not regress."""
66+
assert _annotation(read_geotiff_gpu, 'window') == 'tuple | None'
67+
68+
69+
# --- path: str or binary file-like (writer entry points) ---
70+
71+
72+
def test_to_geotiff_path_annotated():
73+
"""``to_geotiff(data, path, ...)`` ``path`` accepts str or BinaryIO."""
74+
ann = _annotation(to_geotiff, 'path')
75+
assert 'str' in ann
76+
assert 'BinaryIO' in ann
77+
78+
79+
def test_write_geotiff_gpu_path_annotated():
80+
"""``write_geotiff_gpu(data, path, ...)`` ``path`` mirrors ``to_geotiff``."""
81+
ann = _annotation(write_geotiff_gpu, 'path')
82+
assert 'str' in ann
83+
assert 'BinaryIO' in ann
84+
85+
86+
def test_write_vrt_vrt_path_annotated():
87+
"""``write_vrt(vrt_path, ...)`` stays str-only (VRT writes are
88+
path-only by design; no file-like buffer support)."""
89+
assert _annotation(write_vrt, 'vrt_path') == 'str'
90+
91+
92+
# --- on_gpu_failure: 'auto' | 'strict' (GPU failure policy) ---
93+
94+
95+
def test_open_geotiff_on_gpu_failure_annotated():
96+
assert _annotation(open_geotiff, 'on_gpu_failure') == 'str'
97+
98+
99+
def test_read_geotiff_gpu_on_gpu_failure_annotated():
100+
assert _annotation(read_geotiff_gpu, 'on_gpu_failure') == 'str'
101+
102+
103+
def test_read_geotiff_gpu_deprecated_gpu_alias_annotated():
104+
"""The deprecated ``gpu=`` alias on ``read_geotiff_gpu`` carries the
105+
same ``str`` annotation as the new ``on_gpu_failure`` kwarg."""
106+
assert _annotation(read_geotiff_gpu, 'gpu') == 'str'
107+
108+
109+
# --- Smoke: the new annotations do not break runtime call semantics ---
110+
111+
112+
def test_open_geotiff_window_kwarg_runtime(tmp_path):
113+
"""The annotated ``window`` kwarg still accepts a 4-tuple and returns
114+
the requested sub-window. The test does not exercise ``on_gpu_failure``
115+
because the runtime semantics are GPU-only; the annotation itself is
116+
pinned by ``test_open_geotiff_on_gpu_failure_annotated``.
117+
"""
118+
import numpy as np
119+
import xarray as xr
120+
121+
arr = np.arange(64, dtype=np.float32).reshape(8, 8)
122+
da = xr.DataArray(
123+
arr, dims=['y', 'x'],
124+
coords={'y': np.arange(8.0, 0, -1), 'x': np.arange(8.0)},
125+
attrs={'crs': 4326, 'transform': (1.0, 0, 0.0, 0, -1.0, 8.0)},
126+
)
127+
128+
path = str(tmp_path / 'window_kwarg.tif')
129+
to_geotiff(da, path)
130+
r = open_geotiff(path, window=(0, 0, 4, 4))
131+
assert r.shape == (4, 4)

0 commit comments

Comments
 (0)