diff --git a/.claude/sweep-api-consistency-state.csv b/.claude/sweep-api-consistency-state.csv index fbac2b12..928961ef 100644 --- a/.claude/sweep-api-consistency-state.csv +++ b/.claude/sweep-api-consistency-state.csv @@ -1,3 +1,3 @@ module,last_inspected,issue,severity_max,categories_found,notes -geotiff,2026-05-12,1683;1684;1685,MEDIUM,3;5,"Sweep v2 (deep-sweep-api-consistency-geotiff-2026-05-12-v2). 3 MEDIUM findings filed and fixed: #1683 bigtiff docstring gap on to_geotiff (Cat 3, PR #1686); #1684 write_vrt nodata float|None widened to float|int|None (Cat 3, PR #1687); #1685 open_geotiff silent overview_level/on_gpu_failure drop on VRT sources (Cat 5, PR #1689). Prior v1 fix (#1654) covered type-annotation parity across window/path/on_gpu_failure. cuda-validated." +geotiff,2026-05-12,1754,MEDIUM,3,"Sweep v3 (deep-sweep-api-consistency-geotiff-2026-05-12-v3). 1 MEDIUM finding filed and fixed: #1754 open_geotiff source kwarg lacks str|BinaryIO type annotation (Cat 3). PR #1754 adds the annotation + pinning tests for source on all 4 reader entry points. Prior sweep findings (#1683, #1684, #1685, #1654) all confirmed fixed. 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 feffe706..9c50c4ba 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -641,7 +641,7 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None break -def open_geotiff(source, *, dtype=None, +def open_geotiff(source: str | BinaryIO, *, dtype=None, window: tuple | None = None, overview_level: int | None = None, band: int | None = None, diff --git a/xrspatial/geotiff/tests/test_signature_annotations_1654.py b/xrspatial/geotiff/tests/test_signature_annotations_1654.py index 4d292a6c..ec4116b5 100644 --- a/xrspatial/geotiff/tests/test_signature_annotations_1654.py +++ b/xrspatial/geotiff/tests/test_signature_annotations_1654.py @@ -89,6 +89,40 @@ def test_write_vrt_vrt_path_annotated(): assert _annotation(write_vrt, 'vrt_path') == 'str' +# --- source: str or BinaryIO (open_geotiff is the public dispatch) --- + + +def test_open_geotiff_source_annotated(): + """``open_geotiff(source, ...)`` accepts ``str | BinaryIO`` to match + the writer ``path`` annotation and the runtime behaviour the + docstring documents (BytesIO buffers are routed through the eager + numpy reader). The dedicated reader entry points stay ``str``-only + because they reject file-like sources at runtime. See issue #1754. + """ + ann = _annotation(open_geotiff, 'source') + assert 'str' in ann + assert 'BinaryIO' in ann + + +def test_read_geotiff_dask_source_str_only(): + """``read_geotiff_dask(source: str)`` stays str-only: the dask path + reopens the source by path from each worker task and does not + support file-like buffers.""" + assert _annotation(read_geotiff_dask, 'source') == 'str' + + +def test_read_geotiff_gpu_source_str_only(): + """``read_geotiff_gpu(source: str)`` stays str-only: GPU decode + paths read by path / mmap and do not support file-like buffers.""" + assert _annotation(read_geotiff_gpu, 'source') == 'str' + + +def test_read_vrt_source_str_only(): + """``read_vrt(source: str)`` stays str-only: the VRT XML references + its own source files on disk.""" + assert _annotation(read_vrt, 'source') == 'str' + + # --- on_gpu_failure: 'auto' | 'strict' (GPU failure policy) --- @@ -129,3 +163,30 @@ def test_open_geotiff_window_kwarg_runtime(tmp_path): to_geotiff(da, path) r = open_geotiff(path, window=(0, 0, 4, 4)) assert r.shape == (4, 4) + + +def test_open_geotiff_bytesio_source_runtime(tmp_path): + """``open_geotiff`` routes a ``BytesIO`` source through the eager + numpy reader. The annotation pins this contract at the type level; + this test pins it at the runtime level so a future refactor that + drops the file-like branch fails CI. See issue #1754. + """ + import io + 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 / 'bytesio_source.tif') + to_geotiff(da, path) + with open(path, 'rb') as f: + buffer = io.BytesIO(f.read()) + + r = open_geotiff(buffer) + assert r.shape == (8, 8) + assert r.dtype == np.float32