Skip to content

Commit 29c215b

Browse files
committed
Fix write_vrt and write_geotiff_gpu signature/docstring drift vs to_geotiff (#1631)
Three API-consistency drifts surfaced by the sweep on 2026-05-11: 1. write_vrt used **kwargs even though the docstring listed three accepted kwargs (relative, crs_wkt, nodata). inspect.signature, IDE autocomplete, and mypy --strict could not see them. Replaced with an explicit signature that mirrors _vrt.write_vrt and forwards each kwarg by name. A typo'd kwarg now raises TypeError from the public wrapper rather than from deep inside the internal helper. 2. write_geotiff_gpu's overview_resampling docstring omitted 'cubic'. to_geotiff lists it; the underlying make_overview_gpu accepts it (falls back to CPU for parity with the CPU writer). Updated the docstring to match. 3. write_geotiff_gpu(data) was untyped while to_geotiff(data) was annotated xr.DataArray | np.ndarray. Added xr.DataArray | cupy.ndarray for parity. Annotation is a forward reference under the module's `from __future__ import annotations`, so cupy stays lazily imported at function-body level. Regression test: xrspatial/geotiff/tests/test_signature_parity_1631.py pins each guarantee. inspect.signature parity, unknown-kwarg rejection at the public wrapper, cubic-resampling round-trip on the GPU writer.
1 parent cdf7d43 commit 29c215b

2 files changed

Lines changed: 170 additions & 9 deletions

File tree

xrspatial/geotiff/__init__.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2663,7 +2663,7 @@ def _read_once():
26632663
return result
26642664

26652665

2666-
def write_geotiff_gpu(data, path: str, *,
2666+
def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray, path: str, *,
26672667
crs: int | str | None = None,
26682668
nodata=None,
26692669
compression: str = 'zstd',
@@ -2726,7 +2726,10 @@ def write_geotiff_gpu(data, path: str, *,
27262726
halving until the smallest overview fits in a single tile.
27272727
overview_resampling : str
27282728
Resampling method for overviews: 'mean' (default), 'nearest',
2729-
'min', 'max', 'median', or 'mode'.
2729+
'min', 'max', 'median', 'mode', or 'cubic'. ``mode`` and
2730+
``cubic`` fall back to the CPU implementation in
2731+
``xrspatial.geotiff._writer`` so the GPU writer produces the
2732+
same overview bytes as the CPU writer.
27302733
bigtiff : bool or None
27312734
Force BigTIFF (64-bit offsets). None auto-promotes when the
27322735
estimated file size would exceed the classic-TIFF 4 GB limit.
@@ -3187,7 +3190,10 @@ def _sentinel_for_dtype(nodata_val, dtype):
31873190
return result
31883191

31893192

3190-
def write_vrt(vrt_path: str, source_files: list[str], **kwargs) -> str:
3193+
def write_vrt(vrt_path: str, source_files: list[str], *,
3194+
relative: bool = True,
3195+
crs_wkt: str | None = None,
3196+
nodata: float | None = None) -> str:
31913197
"""Generate a VRT file that mosaics multiple GeoTIFF tiles.
31923198
31933199
Parameters
@@ -3208,14 +3214,18 @@ def write_vrt(vrt_path: str, source_files: list[str], **kwargs) -> str:
32083214
-------
32093215
str
32103216
Path to the written VRT file.
3211-
3212-
Notes
3213-
-----
3214-
Only the keyword arguments listed above are accepted. Passing any
3215-
other keyword raises ``TypeError`` from the underlying writer.
32163217
"""
3218+
# Explicit signature (previously ``**kwargs``) so ``inspect.signature``,
3219+
# IDE autocomplete, and ``mypy --strict`` can see the accepted kwargs
3220+
# without parsing the docstring. Mirrors ``_vrt.write_vrt`` exactly; if
3221+
# that signature changes, this wrapper must be updated in lockstep.
32173222
from ._vrt import write_vrt as _write_vrt_internal
3218-
return _write_vrt_internal(vrt_path, source_files, **kwargs)
3223+
return _write_vrt_internal(
3224+
vrt_path, source_files,
3225+
relative=relative,
3226+
crs_wkt=crs_wkt,
3227+
nodata=nodata,
3228+
)
32193229

32203230

32213231
def plot_geotiff(da: xr.DataArray, **kwargs):
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Regression test for #1631: public write_vrt / write_geotiff_gpu
2+
signature and docstring parity vs to_geotiff.
3+
4+
Three drifts were flagged by the api-consistency sweep on 2026-05-11:
5+
6+
1. ``write_vrt(vrt_path, source_files, **kwargs)`` swallowed every kwarg
7+
into ``**kwargs``. The docstring documented ``relative``, ``crs_wkt``,
8+
``nodata``, but ``inspect.signature`` and IDE autocomplete saw nothing.
9+
2. ``write_geotiff_gpu``'s ``overview_resampling`` docstring omitted
10+
``'cubic'``; ``to_geotiff`` lists it and ``make_overview_gpu`` accepts
11+
it (falling back to CPU).
12+
3. ``write_geotiff_gpu(data, ...)`` lacked the type hint that
13+
``to_geotiff(data, ...)`` has.
14+
15+
This module pins each of those three guarantees against future drift.
16+
"""
17+
18+
import inspect
19+
import os
20+
import tempfile
21+
22+
import numpy as np
23+
import pytest
24+
import xarray as xr
25+
26+
from xrspatial.geotiff import (
27+
open_geotiff,
28+
to_geotiff,
29+
write_geotiff_gpu,
30+
write_vrt,
31+
)
32+
33+
34+
def test_write_vrt_signature_exposes_documented_kwargs():
35+
"""``inspect.signature(write_vrt)`` reports the three accepted kwargs.
36+
37+
Prior to #1631 the public wrapper used ``**kwargs``, so
38+
``inspect.signature`` only saw ``vrt_path`` and ``source_files``.
39+
"""
40+
sig = inspect.signature(write_vrt)
41+
params = sig.parameters
42+
assert 'relative' in params
43+
assert 'crs_wkt' in params
44+
assert 'nodata' in params
45+
# Defaults must match _vrt.write_vrt
46+
assert params['relative'].default is True
47+
assert params['crs_wkt'].default is None
48+
assert params['nodata'].default is None
49+
# No more catch-all VAR_KEYWORD
50+
kinds = {p.kind for p in params.values()}
51+
assert inspect.Parameter.VAR_KEYWORD not in kinds
52+
53+
54+
def test_write_vrt_unknown_kwarg_rejected_at_public_level():
55+
"""A typo'd kwarg now raises ``TypeError`` from the public function
56+
rather than from deep inside ``_vrt.write_vrt``.
57+
"""
58+
with tempfile.TemporaryDirectory() as td:
59+
arr = np.zeros((8, 8), dtype=np.float32)
60+
da = xr.DataArray(
61+
arr, dims=['y', 'x'],
62+
coords={'y': np.arange(8.0, 0, -1), 'x': np.arange(8.0)},
63+
attrs={'crs': 4326, 'transform': (1.0, 0, 0.0, 0, -1.0, 8.0)},
64+
)
65+
tif_path = os.path.join(td, 't.tif')
66+
to_geotiff(da, tif_path)
67+
68+
with pytest.raises(TypeError, match='typo_kwarg'):
69+
write_vrt(os.path.join(td, 't.vrt'), [tif_path], typo_kwarg=1)
70+
71+
72+
def test_write_vrt_accepts_documented_kwargs():
73+
"""Each documented kwarg round-trips through the explicit signature."""
74+
with tempfile.TemporaryDirectory() as td:
75+
arr = np.zeros((8, 8), dtype=np.float32)
76+
da = xr.DataArray(
77+
arr, dims=['y', 'x'],
78+
coords={'y': np.arange(8.0, 0, -1), 'x': np.arange(8.0)},
79+
attrs={'crs': 4326, 'transform': (1.0, 0, 0.0, 0, -1.0, 8.0)},
80+
)
81+
tif_path = os.path.join(td, 't.tif')
82+
to_geotiff(da, tif_path)
83+
84+
vrt_path = os.path.join(td, 't.vrt')
85+
out = write_vrt(
86+
vrt_path, [tif_path],
87+
relative=False, crs_wkt=None, nodata=-9999.0,
88+
)
89+
assert out == vrt_path
90+
assert os.path.exists(vrt_path)
91+
92+
93+
def test_write_geotiff_gpu_docstring_lists_cubic():
94+
"""``overview_resampling`` docstring includes ``'cubic'`` so it
95+
matches ``to_geotiff`` and the underlying ``make_overview_gpu``.
96+
"""
97+
doc = write_geotiff_gpu.__doc__
98+
assert doc is not None
99+
# Find the overview_resampling block
100+
assert 'overview_resampling' in doc
101+
# The block must mention cubic
102+
block_start = doc.index('overview_resampling')
103+
block_end = doc.index('bigtiff', block_start)
104+
block = doc[block_start:block_end]
105+
assert 'cubic' in block
106+
107+
108+
def test_write_geotiff_gpu_data_has_type_hint():
109+
"""``data`` parameter is annotated, matching ``to_geotiff(data, ...)``."""
110+
sig = inspect.signature(write_geotiff_gpu)
111+
data_param = sig.parameters['data']
112+
assert data_param.annotation is not inspect.Parameter.empty
113+
# The annotation is a forward reference under ``from __future__ import
114+
# annotations``; just confirm it mentions the documented types.
115+
ann_str = str(data_param.annotation)
116+
assert 'DataArray' in ann_str or 'cupy' in ann_str
117+
118+
119+
@pytest.mark.skipif(
120+
not pytest.importorskip('cupy', reason='cupy required').is_available()
121+
if False else False,
122+
reason='guarded below',
123+
)
124+
def test_write_geotiff_gpu_cubic_overview_round_trip():
125+
"""``overview_resampling='cubic'`` works on the GPU writer.
126+
127+
Sanity check that the docstring update is not advertising an
128+
unsupported codec. ``make_overview_gpu`` falls back to the CPU
129+
cubic implementation for parity with the CPU writer.
130+
"""
131+
cupy = pytest.importorskip('cupy')
132+
try:
133+
cupy.zeros(1)
134+
except Exception:
135+
pytest.skip('cupy import succeeded but no device available')
136+
137+
with tempfile.TemporaryDirectory() as td:
138+
arr_cpu = np.random.RandomState(0).rand(256, 256).astype(np.float32)
139+
arr_gpu = cupy.asarray(arr_cpu)
140+
da_gpu = xr.DataArray(
141+
arr_gpu, dims=['y', 'x'],
142+
coords={'y': np.arange(256.0, 0, -1), 'x': np.arange(256.0)},
143+
)
144+
path = os.path.join(td, 'cog.tif')
145+
write_geotiff_gpu(
146+
da_gpu, path,
147+
cog=True, tile_size=64, overview_resampling='cubic',
148+
)
149+
# Overview level 1 = 1/2 resolution
150+
ov = open_geotiff(path, overview_level=1)
151+
assert ov.shape == (128, 128)

0 commit comments

Comments
 (0)