Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .claude/sweep-api-consistency-state.csv
Original file line number Diff line number Diff line change
@@ -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)."
23 changes: 16 additions & 7 deletions xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
131 changes: 131 additions & 0 deletions xrspatial/geotiff/tests/test_signature_annotations_1654.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""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_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

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)},
)

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)
Loading