diff --git a/README.md b/README.md index 640910295..cf1bdee5f 100644 --- a/README.md +++ b/README.md @@ -163,13 +163,14 @@ VRT is supported as a conservative advanced feature for simple GeoTIFF mosaics, |:-----|:------------|:-----:|:----:|:--------:|:-------------:|:-----:| | [open_geotiff](xrspatial/geotiff/__init__.py) | Read GeoTIFF / COG / VRT | โœ… | โœ… | ๐Ÿงช | ๐Ÿงช | ๐Ÿ”ผ | | [to_geotiff](xrspatial/geotiff/__init__.py) | Write DataArray as GeoTIFF / COG | โœ… | โœ… | ๐Ÿงช | ๐Ÿงช | ๐Ÿ”ผ | -| [write_geotiff_gpu](xrspatial/geotiff/__init__.py) | GPU-accelerated GeoTIFF / COG write | | | ๐Ÿงช | | | -| [write_vrt](xrspatial/geotiff/__init__.py) | Generate VRT mosaic from GeoTIFFs | ๐Ÿ”ผ | | | | | +| [build_vrt](xrspatial/geotiff/__init__.py) | Generate VRT mosaic from existing GeoTIFFs | ๐Ÿ”ผ | | | | | -`open_geotiff` and `to_geotiff` auto-dispatch to the correct backend: +`open_geotiff` and `to_geotiff` select the backend from their parameters +(`gpu=`, `chunks=`, `.vrt` path); GPU read/write is reached with `gpu=True`, +not a separate function: ```python -from xrspatial.geotiff import open_geotiff, to_geotiff +from xrspatial.geotiff import build_vrt, open_geotiff, to_geotiff open_geotiff('dem.tif') # NumPy open_geotiff('dem.tif', chunks=512) # Dask @@ -186,7 +187,7 @@ to_geotiff(data, 'cog.tif', cog=True) # COG with auto overviews to_geotiff(data, 'cog.tif', cog=True, # COG with explicit levels overview_levels=[2, 4, 8], overview_resampling='nearest') -write_vrt('mosaic.vrt', ['tile1.tif', 'tile2.tif']) # generate VRT +build_vrt('mosaic.vrt', ['tile1.tif', 'tile2.tif']) # mosaic existing tiles open_geotiff('dem.tif', dtype='float32') # half memory open_geotiff('dem.tif', dtype='float32', chunks=512) # Dask + half memory diff --git a/docs/source/reference/geotiff.rst b/docs/source/reference/geotiff.rst index a8b4ed70b..771e29ff3 100644 --- a/docs/source/reference/geotiff.rst +++ b/docs/source/reference/geotiff.rst @@ -51,8 +51,8 @@ What you should NOT rely on: * GPU support for every codec on the CPU path. ``allow_experimental_codecs`` does NOT widen the GPU codec set; on the GPU writer, codecs outside the - GPU-supported set route through a CPU fallback inside - ``write_geotiff_gpu`` rather than executing on the GPU. Locked by + GPU-supported set route through a CPU fallback inside the GPU writer + (``_write_geotiff_gpu``) rather than executing on the GPU. Locked by ``xrspatial/geotiff/tests/gpu/test_writer.py``. * GPU promotion to ``stable`` inside this release cycle. See the GPU rows in :ref:`reference.geotiff_release_gate` for the current tier @@ -206,20 +206,26 @@ The lifecycle is locked end-to-end by Reading ======= +``open_geotiff`` is the single read entry point. The backend follows the +parameters: ``gpu=True`` returns a CuPy-backed array, ``chunks=N`` returns a +lazy dask array, and a ``.vrt`` source reads a mosaic. + .. autosummary:: :toctree: _autosummary xrspatial.geotiff.open_geotiff - xrspatial.geotiff.read_vrt Writing ======= +``to_geotiff`` is the single write entry point (``gpu=True`` or CuPy data +selects the GPU path; a ``.vrt`` output path writes tiles plus an index). +``build_vrt`` mosaics a list of existing GeoTIFF files into a VRT. + .. autosummary:: :toctree: _autosummary xrspatial.geotiff.to_geotiff - xrspatial.geotiff.write_geotiff_gpu - xrspatial.geotiff.write_vrt + xrspatial.geotiff.build_vrt COG validator CI gate ===================== @@ -346,7 +352,7 @@ with the original exception type and message. Set ``XRSPATIAL_GEOTIFF_STRICT=1`` (or ``true``, ``yes``) to promote those warnings into raised exceptions. The same env var also forces -``read_geotiff_gpu(on_gpu_failure='auto')`` to behave like +``open_geotiff(gpu=True, on_gpu_failure='auto')`` to behave like ``on_gpu_failure='strict'`` so CI can fail loudly when the GPU fast path silently falls back to CPU. @@ -393,9 +399,9 @@ VRT support matrix (issue #2321) VRT reads sit at the ``advanced`` tier in :data:`xrspatial.geotiff.SUPPORTED_FEATURES` (``reader.vrt``). -``open_geotiff``, ``read_vrt``, and ``write_vrt`` all target the same -narrow subset of GDAL's VRT spec. The reference below is the canonical -contract; the three docstrings echo it. +``open_geotiff`` (on a ``.vrt`` source), ``to_geotiff`` (to a ``.vrt`` +output), and ``build_vrt`` all target the same narrow subset of GDAL's VRT +spec. The reference below is the canonical contract; the docstrings echo it. Supported --------- @@ -404,7 +410,7 @@ Supported GeoTIFF files. The VRT XML must resolve to source paths under the VRT's own directory (or under a root listed in ``XRSPATIAL_VRT_ALLOWED_ROOTS``); see the source-path containment - note on ``read_vrt`` (#1671). + note on the VRT reader (``_read_vrt``) (#1671). * Sources that agree on CRS, transform orientation (axis-aligned, same sign on the y step), pixel size, dtype, and band count. The read rejects mismatch with ``MixedBandMetadataError`` / @@ -450,11 +456,11 @@ the fail-closed defaults: .. code-block:: python - from xrspatial.geotiff import open_geotiff, write_vrt + from xrspatial.geotiff import build_vrt, open_geotiff # Write a VRT that mosaics two tiles. Both tiles share CRS, # pixel size, dtype, and band count. - vrt_path = write_vrt( + vrt_path = build_vrt( 'mosaic.vrt', source_files=['tile_west.tif', 'tile_east.tif'], ) @@ -486,7 +492,8 @@ per-band nodata sentinels triggers the fail-closed check: VRT missing sources =================== -``read_vrt`` accepts ``missing_sources='warn'`` or ``'raise'``. The default +``open_geotiff`` accepts ``missing_sources='warn'`` or ``'raise'`` for +``.vrt`` sources. The default ``'raise'`` (since #1860) fails the read immediately if any source file referenced by the VRT does not exist on disk. Both the eager and chunked dispatchers honour this at construction time -- chunked callers do not diff --git a/docs/source/reference/geotiff_internals.md b/docs/source/reference/geotiff_internals.md index c86f66d4c..99cd4ff6a 100644 --- a/docs/source/reference/geotiff_internals.md +++ b/docs/source/reference/geotiff_internals.md @@ -18,17 +18,17 @@ public API. Files referenced live under `xrspatial/geotiff/`. | Entry point | File | Returns | | -------------------- | --------------------------------- | ---------------------- | | `open_geotiff` | `xrspatial/geotiff/__init__.py` | dispatcher (NumPy / CuPy / Dask / Dask+CuPy / VRT) | -| `read_geotiff_dask` | `xrspatial/geotiff/_backends/dask.py` | Dask-NumPy DataArray | -| `read_geotiff_gpu` | `xrspatial/geotiff/_backends/gpu.py` | CuPy or Dask-CuPy DataArray | -| `read_vrt` | `xrspatial/geotiff/_backends/vrt.py` | NumPy / CuPy / Dask DataArray (mosaic) | +| `_read_geotiff_dask` | `xrspatial/geotiff/_backends/dask.py` | Dask-NumPy DataArray | +| `_read_geotiff_gpu` | `xrspatial/geotiff/_backends/gpu.py` | CuPy or Dask-CuPy DataArray | +| `_read_vrt` | `xrspatial/geotiff/_backends/vrt.py` | NumPy / CuPy / Dask DataArray (mosaic) | ### Write | Entry point | File | Input | | -------------------- | --------------------------------- | ---------------------- | | `to_geotiff` | `xrspatial/geotiff/_writers/eager.py` | NumPy / Dask DataArray (auto-dispatches to GPU when input is CuPy-backed) | -| `write_geotiff_gpu` | `xrspatial/geotiff/_writers/gpu.py` | CuPy DataArray | -| `write_vrt` | `xrspatial/geotiff/_writers/vrt.py` | list of GeoTIFF paths (XML emitter) | +| `_write_geotiff_gpu` | `xrspatial/geotiff/_writers/gpu.py` | CuPy DataArray | +| `build_vrt` | `xrspatial/geotiff/_writers/vrt.py` | list of GeoTIFF paths (XML emitter) | ## Contract steps @@ -122,12 +122,12 @@ now and that the call-site comments justify. ### Read backends -| Step | `open_geotiff` (eager) | `read_geotiff_dask` | `read_geotiff_gpu` (eager) | `read_geotiff_gpu` (chunked) | `read_vrt` (eager) | `read_vrt` (chunked) | +| Step | `open_geotiff` (eager) | `_read_geotiff_dask` | `_read_geotiff_gpu` (eager) | `_read_geotiff_gpu` (chunked) | `_read_vrt` (eager) | `_read_vrt` (chunked) | | ---- | ---------------------- | ------------------- | -------------------------- | ---------------------------- | ------------------ | -------------------- | | 1. source / kwarg validation | shared (`_validate_dispatch_kwargs` then dispatches) | shared (`_validate_dispatch_kwargs`, `_validate_chunks_arg`) | shared (`_validate_dispatch_kwargs`, `_validate_chunks_arg`) | shared (`_validate_dispatch_kwargs`, `_validate_chunks_arg`) | shared (`_validate_dispatch_kwargs`, `_validate_chunks_arg`); duplicated inline overview-level / `missing_sources` / `band_nodata` value rejections | shared (`_validate_dispatch_kwargs`); duplicated inline overview-level / `missing_sources` / `band_nodata` value rejections | | 2. metadata parse | shared (`read_to_array` -> `_parse_cog_http_meta` for cloud (with `.tif.ovr` sidecar discovery via `discover_remote_sidecar`), `parse_header` + `parse_all_ifds` + sidecar otherwise) | shared (`_read_geo_info` for local, `_parse_cog_http_meta` for HTTP/fsspec, both with `.tif.ovr` sidecar discovery via `discover_remote_sidecar` -- #2239) | shared (`extract_geo_info_with_overview_inheritance`, `select_overview_ifd`); duplicated inline IFD + sidecar load lifted from `_read_geo_info` | shared (`extract_geo_info_with_overview_inheritance`); duplicated inline IFD + sidecar handling | duplicated (`_parse_vrt` + `_read_vrt_internal` -- VRT-specific, no shared metadata parser) | duplicated (`_parse_vrt` + per-chunk `_vrt_chunk_read`) | | 3. transform / georef classification | shared (`_populate_attrs_from_geo_info` via `_finalize_eager_read`) | shared (`_populate_attrs_from_geo_info` via `_finalize_lazy_read_attrs`) | shared (`_populate_attrs_from_geo_info` via `_finalize_eager_read`) | shared (`_populate_attrs_from_geo_info` via `_finalize_lazy_read_attrs`) | shared (`_vrt_to_synthetic_geo_info` -> `_finalize_lazy_read_attrs`); documented divergence: per-band nodata sentinel selection runs before the helper, and `vrt_holes` is injected through `attrs_in` because `GeoInfo` has no slot for it | shared (`_vrt_to_synthetic_geo_info` -> `_finalize_lazy_read_attrs`); same documented divergence | -| 4. pixel decode | shared (`read_to_array`) | shared (per-chunk `read_to_array` / `_fetch_decode_cog_http_tiles`) | duplicated (inline GDS / KvikIO / nvCOMP path with CPU fallback via `read_to_array`) | duplicated (inline GDS + per-chunk delayed; HTTP / fsspec / stripped layouts fall back to `read_geotiff_dask`) | duplicated (`_read_vrt_internal._read_data` per source) | duplicated (per-chunk `_vrt_chunk_read` decodes only sources intersecting the window) | +| 4. pixel decode | shared (`read_to_array`) | shared (per-chunk `read_to_array` / `_fetch_decode_cog_http_tiles`) | duplicated (inline GDS / KvikIO / nvCOMP path with CPU fallback via `read_to_array`) | duplicated (inline GDS + per-chunk delayed; HTTP / fsspec / stripped layouts fall back to `_read_geotiff_dask`) | duplicated (`_read_vrt_internal._read_data` per source) | duplicated (per-chunk `_vrt_chunk_read` decodes only sources intersecting the window) | | 5. orientation / photometric | shared (`read_to_array` applies both) | shared (per chunk via `read_to_array`); rejects non-default orientation on HTTP COG dask path | shared on CPU-fallback (`read_to_array`); duplicated on pure GPU path (`_apply_orientation_gpu`, `_apply_orientation_geo_info`, inline MinIsWhite inversion) | shared on CPU-fallback; duplicated on disk-to-GPU per-chunk path (`_decode_window_gpu_direct`); rejects orientation != 1 in `_gds_chunk_path_available` | duplicated (inline NaN masking in `_vrt._read_data` for float sources; VRT does not carry an orientation tag) | duplicated (per chunk same as eager VRT) | | 6. nodata mask + dtype cast | shared (`_apply_eager_nodata_mask` + `_validate_dtype_cast` via `_finalize_eager_read`) | duplicated (per-chunk mask inline in `_delayed_read_window`); shared `_validate_dtype_cast` on graph dtype | shared (`_apply_eager_nodata_mask` via `_finalize_eager_read`) on both stripped and tiled paths | duplicated (per-chunk mask inline in `_chunk_task`); shared `_validate_dtype_cast` | duplicated (`_apply_integer_sentinel_mask_with_presence` for per-band integer sentinels, plus inline float-NaN proxy and pre-cast dtype tracking); shared `_validate_dtype_cast` | duplicated (per-chunk integer sentinel mask via `_apply_integer_sentinel_mask_with_presence`); shared `_validate_dtype_cast` | | 7. attrs finalization | shared (`_finalize_eager_read` -> `_validate_read_geo_info` + `_populate_attrs_from_geo_info` + `_set_nodata_attrs`) | shared (`_finalize_lazy_read_attrs`); documented divergence: `nodata_pixels_present` stays unset on lazy outputs (issue #2135) | shared (`_finalize_eager_read`); GPU MinIsWhite picks `mask_sentinel` from three local stashes (`_mw_mask_nodata`, `_cpu_fallback_geo._mask_nodata`, or raw `nodata`) | shared (`_finalize_lazy_read_attrs`); same `nodata_pixels_present` divergence as the CPU dask path | shared (`_finalize_lazy_read_attrs`); documented divergences: `vrt_holes` injected via `attrs_in` seed; per-band nodata selection runs before the helper; `nodata_pixels_present` stamped post-helper from a VRT-aware scan (`_vrt_mask_with_presence` / `_vrt_scan_for_sentinel`) | shared (`_finalize_lazy_read_attrs`); same VRT divergences as the eager VRT path | @@ -138,11 +138,11 @@ now and that the call-site comments justify. The TIFF write contract is the inverse of the read contract: validate the DataArray, resolve transform / CRS / nodata from the attrs, lay out the output, encode, and emit bytes. Steps 4 and 5 (decode, orientation) have no -write analogue; `to_geotiff` and `write_geotiff_gpu` always emit +write analogue; `to_geotiff` and `_write_geotiff_gpu` always emit Orientation = 1 and rely on the writer assembler (`_writer.write`) for photometric handling. -| Step | `to_geotiff` (CPU eager / dask) | `write_geotiff_gpu` | `write_vrt` | +| Step | `to_geotiff` (CPU eager / dask) | `_write_geotiff_gpu` | `build_vrt` | | ---- | ------------------------------- | ------------------- | ----------- | | 1. source / kwarg validation | shared (`_validate_tile_size_arg`, `_validate_3d_writer_dims`, `_validate_writer_spatial_shape`, `_validate_nodata_arg`, `_validate_no_rotated_affine`); duplicated inline compression / `compression_level` / `cog` / `overview_levels` / `bigtiff` / `streaming_buffer_bytes` / `max_z_error` / `photometric` / `allow_internal_only_jpeg` / `allow_experimental_codecs` value rejections | shared (`_validate_tile_size_arg`, `_validate_3d_writer_dims`, `_validate_writer_spatial_shape`, `_validate_nodata_arg`, `_validate_no_rotated_affine`); duplicated inline GPU-specific kwarg rejections (`predictor`, `compression`, `cog`, etc.) | shared (`_validate_nodata_arg`); duplicated inline `path` / `vrt_path` shim, `crs` / `crs_wkt` shim, source path validation | | 2. metadata parse | N/A (no source to parse; reads attrs off the DataArray) | N/A | duplicated (reads geokeys from the first source file to inherit CRS / nodata; lives in `_vrt.write_vrt`) | diff --git a/docs/source/reference/release_gate_geotiff.rst b/docs/source/reference/release_gate_geotiff.rst index c14cee926..75c919005 100644 --- a/docs/source/reference/release_gate_geotiff.rst +++ b/docs/source/reference/release_gate_geotiff.rst @@ -180,7 +180,7 @@ Local GeoTIFF read and write ``T_full * Affine.translation(col_off, row_off)`` (no float drift), and the canonical non-transform release attrs unchanged. Covered for both ``open_geotiff(window=...)`` and - ``read_geotiff_dask(window=...)``. + ``open_geotiff(window=..., chunks=...)``. - ``xrspatial/geotiff/tests/release_gates/test_stable_features.py`` (windowed-reads section) - `#2341`_ @@ -194,7 +194,7 @@ Local GeoTIFF read and write - `#2341`_ * - ``reader.dask`` -- eager / dask parity - stable - - ``open_geotiff(path)`` and ``read_geotiff_dask(path)`` return the + - ``open_geotiff(path)`` and ``open_geotiff(path, chunks=...)`` return the same pixels, ``dims``, ``coords``, and the seven release-attr keys (``transform``, ``crs``, ``crs_wkt``, ``nodata``, ``masked_nodata``, ``georef_status``, ``raster_type``) across @@ -557,7 +557,7 @@ VRT supported subset - ``xrspatial/geotiff/tests/release_gates/test_stable_features.py`` (VRT presence meta-gate) - `#2321`_ - * - ``write_vrt`` + * - ``build_vrt`` - advanced - Writer rejects source-incompatibility cases at the writer boundary. - ``xrspatial/geotiff/tests/vrt/test_validation.py`` diff --git a/docs/source/user_guide/geotiff_safe_io.rst b/docs/source/user_guide/geotiff_safe_io.rst index fb81ba0fb..a2661d853 100644 --- a/docs/source/user_guide/geotiff_safe_io.rst +++ b/docs/source/user_guide/geotiff_safe_io.rst @@ -39,12 +39,9 @@ the read and write paths: :class:`xarray.DataArray` for single-band input and a 3D one for multi-band input. The binary file-like form is restricted to the eager numpy reader; dask, GPU, VRT, and remote-URL paths require - a string. - * - :func:`xrspatial.geotiff.read_vrt` - - Dedicated entry point for reading a GDAL ``.vrt`` mosaic over a - set of GeoTIFF sources. Tier: ``advanced``. The VRT path honours - a documented subset of the GDAL VRT schema; unsupported features - raise ``VRTUnsupportedError`` or + a string. A ``.vrt`` source reads a GDAL mosaic (tier: + ``advanced``) over a documented subset of the GDAL VRT schema; + unsupported features raise ``VRTUnsupportedError`` or :class:`xrspatial.geotiff.UnsupportedGeoTIFFFeatureError` at graph-build time rather than producing wrong pixels. Both error classes live in :mod:`xrspatial.geotiff._errors`. @@ -53,13 +50,13 @@ the read and write paths: Cloud-optimized GeoTIFF layout. Pass ``allow_experimental_codecs=True`` to opt into ``lerc``, ``jpeg2000`` / ``j2k``, or ``lz4``; pass ``allow_internal_only_jpeg=True`` to opt into the - internal-only ``jpeg`` codec. - * - :func:`xrspatial.geotiff.write_geotiff_gpu` - - GPU writer. Tier: ``experimental``. Use the CPU writer for - anything you intend to round-trip through external tools. - * - :func:`xrspatial.geotiff.write_vrt` - - Emit a GDAL ``.vrt`` over local GeoTIFF sources. Tier: - ``advanced``. + internal-only ``jpeg`` codec. Pass ``gpu=True`` (or pass + CuPy-backed data) for the GPU writer (tier: ``experimental``); + use the CPU path for anything you round-trip through external + tools. + * - :func:`xrspatial.geotiff.build_vrt` + - Emit a GDAL ``.vrt`` over a list of existing local GeoTIFF + sources. Tier: ``advanced``. A dask-backed read is just ``open_geotiff(source, chunks=...)`` -- there is no separate ``read_geotiff_dask`` name on the public surface. The diff --git a/examples/user_guide/39_GeoTIFF_IO.ipynb b/examples/user_guide/39_GeoTIFF_IO.ipynb index dcbf66875..7eaea59e5 100644 --- a/examples/user_guide/39_GeoTIFF_IO.ipynb +++ b/examples/user_guide/39_GeoTIFF_IO.ipynb @@ -15,7 +15,7 @@ "source": [ "### Tier note\n", "\n", - "`open_geotiff` and `to_geotiff` against local files, the lossless codecs (`none`, `deflate`, `lzw`, `zstd`, `packbits`), and axis-aligned 2D / 3D rasters are tagged `stable` in `xrspatial.geotiff.SUPPORTED_FEATURES`. Dask reads (`reader.dask`) and dask streaming writes (covered by `writer.local_file`) are stable too. The VRT mosaic section at the bottom exercises `write_vrt` / `read_vrt`, which sit at the `advanced` tier (`reader.vrt`): the supported subset is a flat mosaic of compatible GeoTIFF tiles, not the full GDAL VRT spec.\n", + "`open_geotiff` and `to_geotiff` against local files, the lossless codecs (`none`, `deflate`, `lzw`, `zstd`, `packbits`), and axis-aligned 2D / 3D rasters are tagged `stable` in `xrspatial.geotiff.SUPPORTED_FEATURES`. Dask reads (`reader.dask`) and dask streaming writes (covered by `writer.local_file`) are stable too. The VRT mosaic section at the bottom exercises `build_vrt` and reads the mosaic back with `open_geotiff`, which sit at the `advanced` tier (`reader.vrt`): the supported subset is a flat mosaic of compatible GeoTIFF tiles, not the full GDAL VRT spec.\n", "\n", "**See also:** the GeoTIFF / COG reference page at `docs/source/reference/geotiff.rst` lists every feature in `xrspatial.geotiff.SUPPORTED_FEATURES` against its tier (`stable`, `advanced`, `experimental`, `internal_only`) and links the release gate that locks each promise.\n" ] @@ -29,7 +29,7 @@ "1. [Write and read back a GeoTIFF](#Write-and-read-back) with `to_geotiff` and `open_geotiff`\n", "2. [Write from a DataArray accessor](#Accessor-write) using `da.xrs.to_geotiff()`\n", "3. [Windowed read via Dataset accessor](#Windowed-read-via-Dataset) using `ds.xrs.open_geotiff()` to crop a large file to an existing spatial extent\n", - "4. [Stitch tiles with write_vrt](#VRT-mosaic) to build a virtual mosaic from multiple GeoTIFFs\n", + "4. [Stitch tiles with build_vrt](#VRT-mosaic) to build a virtual mosaic from multiple GeoTIFFs\n", "\n", "![GeoTIFF I/O preview](images/geotiff_io_preview.png)" ] @@ -64,7 +64,7 @@ "import matplotlib.pyplot as plt\n", "\n", "import xrspatial\n", - "from xrspatial.geotiff import open_geotiff, to_geotiff, write_vrt" + "from xrspatial.geotiff import open_geotiff, to_geotiff, build_vrt" ] }, { @@ -918,7 +918,7 @@ "source": [ "## VRT mosaic\n", "\n", - "`write_vrt` writes a lightweight XML file that stitches multiple GeoTIFFs into one virtual raster. The tiles aren't copied, just referenced." + "`build_vrt` writes a lightweight XML file that stitches multiple GeoTIFFs into one virtual raster. The tiles aren't copied, just referenced." ] }, { @@ -965,7 +965,7 @@ "\n", "# Stitch into a VRT\n", "vrt_path = os.path.join(tmpdir, 'mosaic.vrt')\n", - "write_vrt(vrt_path, tile_paths)\n", + "build_vrt(vrt_path, tile_paths)\n", "print(f'\\nVRT: {os.path.getsize(vrt_path):,} bytes')\n", "\n", "# Read the mosaic back\n", @@ -993,7 +993,7 @@ "\n", "- **stable** -- the path a new user should be on. Local file in, local file out, lossless codec, axis-aligned grid.\n", "- **advanced** -- works and is tested, but the caller should know the failure mode (cloud cost, partial VRT mosaics, rotated transforms drop on write, BigTIFF promotion, etc.).\n", - "- **experimental** -- no claim about cross-backend numerical parity or external interop. Tier 3 codecs (`lerc`, `jpeg2000` / `j2k`, `lz4`) require `allow_experimental_codecs=True` on `to_geotiff` and `write_geotiff_gpu`; the GPU read/write paths use `gpu=True` as their explicit opt-in.\n", + "- **experimental** -- no claim about cross-backend numerical parity or external interop. Tier 3 codecs (`lerc`, `jpeg2000` / `j2k`, `lz4`) require `allow_experimental_codecs=True` on `to_geotiff`; the GPU read/write paths use `gpu=True` as their explicit opt-in.\n", "- **internal_only** -- the strictest tier. `compression='jpeg'` writes self-contained JFIF tiles without the TIFF JPEGTables tag, so the output decodes through xrspatial but not libtiff / GDAL / rasterio. Requires the dedicated `allow_internal_only_jpeg=True` flag (issue #1845); `allow_experimental_codecs` does not cover it." ] }, diff --git a/xrspatial/geotiff/__init__.py b/xrspatial/geotiff/__init__.py index 2e956d5aa..93eed2231 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -5,31 +5,28 @@ Public API ---------- open_geotiff(source, ...) - Read a GeoTIFF, COG, or VRT file to an xarray.DataArray. Auto-dispatches - to the GPU, dask, or numpy backend based on the ``gpu`` and ``chunks`` - kwargs. -read_geotiff_gpu(source, ...) - GPU-only read returning a CuPy-backed DataArray. ``open_geotiff(..., - gpu=True)`` calls this internally; use the explicit name when you want - the strict-mode failure semantics (``on_gpu_failure='strict'``) or want - to bypass auto-dispatch. -read_geotiff_dask(source, ...) - Dask-only read returning a windowed lazy DataArray. ``open_geotiff(..., - chunks=N)`` calls this internally. -read_vrt(source, ...) - Read a GDAL Virtual Raster Table (.vrt). ``open_geotiff`` routes ``.vrt`` - paths here automatically; the explicit entry point is useful for - callers that already know they have a VRT. + Read a GeoTIFF, COG, or VRT file to an xarray.DataArray. The backend is + chosen from the parameters: ``gpu=True`` returns a CuPy-backed array, + ``chunks=N`` returns a windowed lazy dask array, a ``.vrt`` source reads + a GDAL Virtual Raster Table, and the default is an eager numpy read. to_geotiff(data, path, ...) - Write an xarray.DataArray as a GeoTIFF or COG. Auto-dispatches to GPU - when the data is CuPy-backed. -write_geotiff_gpu(data, path, ...) - GPU-only writer using nvCOMP. ``to_geotiff(..., gpu=True)`` calls this - internally. -write_vrt(path, source_files, ...) - Generate a VRT mosaic XML from a list of GeoTIFF files. ``vrt_path`` - is kept as a deprecated alias for ``path``; passing both ``path`` and - ``vrt_path`` raises ``TypeError``. + Write an xarray.DataArray as a GeoTIFF or COG. The backend is chosen + from the data and parameters: CuPy-backed data or ``gpu=True`` writes + through the GPU (nvCOMP) path, a ``.vrt`` output path writes a directory + of tiled GeoTIFFs plus a VRT index, and the default is an eager CPU + write. +build_vrt(path, source_files, ...) + Generate a VRT mosaic XML from a list of existing GeoTIFF files. This + is the one read/write helper that does not fold into ``to_geotiff`` + because it has no DataArray to write -- it indexes files that already + exist. ``vrt_path`` is kept as a deprecated alias for ``path``; passing + both ``path`` and ``vrt_path`` raises ``TypeError``. + +The backend functions ``_read_geotiff_gpu``, ``_read_geotiff_dask``, +``_read_vrt``, and ``_write_geotiff_gpu`` are private. ``open_geotiff`` and +``to_geotiff`` dispatch to them. They are bound on the package +(``xrspatial.geotiff._read_geotiff_gpu``) and also importable from their +backend modules; reach for them only to bypass auto-dispatch. """ from __future__ import annotations @@ -57,9 +54,9 @@ # Re-export only; called by xrspatial/geotiff/tests/test_nodata_*.py. from ._backends._gpu_helpers import _apply_nodata_mask_gpu # noqa: F401 from ._backends._gpu_helpers import _is_gpu_data # noqa: F401 -from ._backends.dask import read_geotiff_dask -from ._backends.gpu import read_geotiff_gpu -from ._backends.vrt import read_vrt +from ._backends.dask import _read_geotiff_dask +from ._backends.gpu import _read_geotiff_gpu +from ._backends.vrt import _read_vrt from ._coords import _BAND_DIM_NAMES # noqa: F401 from ._coords import coords_from_pixel_geometry as _coords_from_pixel_geometry # noqa: F401 from ._coords import coords_to_transform as _coords_to_transform # noqa: F401 @@ -93,8 +90,11 @@ # the handful of internal call sites that need it. from ._writers.eager import _write_single_tile # noqa: F401 from ._writers.eager import to_geotiff -from ._writers.gpu import write_geotiff_gpu -from ._writers.vrt import write_vrt +# Re-export only: bound on the package so ``xrspatial.geotiff._write_geotiff_gpu`` +# resolves for tests that monkeypatch it and callers bypassing auto-dispatch. +# ``to_geotiff`` reaches it via ``_writers.eager``; not called here directly. +from ._writers.gpu import _write_geotiff_gpu # noqa: F401 +from ._writers.vrt import build_vrt # All names below are part of the supported public API. ``plot_geotiff`` # is intentionally omitted: it is deprecated in favour of ``da.xrs.plot()`` @@ -125,13 +125,9 @@ 'UnsafeURLError', 'UnsupportedGeoTIFFFeatureError', 'VRTStableSourcesOnlyError', + 'build_vrt', 'open_geotiff', - 'read_geotiff_gpu', - 'read_geotiff_dask', - 'read_vrt', 'to_geotiff', - 'write_geotiff_gpu', - 'write_vrt', ] @@ -633,26 +629,26 @@ def open_geotiff(source: str | BinaryIO, *, or a ``.vrt`` source raises ``ValueError`` because those backends do not apply the cloud-byte budget. on_gpu_failure : {'auto', 'strict'}, optional - [experimental] Forwarded to ``read_geotiff_gpu`` when + [experimental] Forwarded to ``_read_geotiff_gpu`` when ``gpu=True``. Controls whether GPU decode failures fall back to CPU (``'auto'``, default) or re-raise the original exception (``'strict'``). Passing this kwarg with ``gpu=False`` raises ``ValueError`` because the policy only applies to the GPU - pipeline. See ``read_geotiff_gpu`` for the full description. + pipeline. See ``_read_geotiff_gpu`` for the full description. missing_sources : {'raise', 'warn'}, optional [advanced] VRT mosaics can return partial output under ``missing_sources='warn'`` when a backing source is unreadable; the ``attrs['vrt_holes']`` entry records which sources were skipped so downstream code can detect the partial mosaic. - Forwarded to ``read_vrt`` when the source is a ``.vrt`` file. + Forwarded to ``_read_vrt`` when the source is a ``.vrt`` file. When the caller does not pass this kwarg, the public - ``read_vrt`` default applies (``'raise'``). + ``_read_vrt`` default applies (``'raise'``). ``'raise'`` fails immediately on an unreadable backing source. ``'warn'`` is the opt-in lenient mode: emit ``GeoTIFFFallbackWarning``, record ``attrs['vrt_holes']``, and return a partial mosaic. Passing this kwarg with a non-VRT source raises ``ValueError`` because the policy only applies to - the VRT pipeline. See ``read_vrt`` for the full description. + the VRT pipeline. See ``_read_vrt`` for the full description. band_nodata : {'first', None}, optional [advanced] VRT-only. Opt-out for the fail-closed check that rejects VRT sources whose bands declare disagreeing per-band @@ -697,7 +693,7 @@ def open_geotiff(source: str | BinaryIO, *, rotated rasters can recover the mapping. The contract is read-only -- writes must either reproject onto an axis-aligned grid first, or pass ``drop_rotation=True`` to - ``to_geotiff`` / ``write_geotiff_gpu`` to accept the loss; the + ``to_geotiff`` / ``_write_geotiff_gpu`` to accept the loss; the ``ModelTransformationTag`` emit path is tracked separately. allow_unparseable_crs : bool, default False [advanced] Read-side opt-in for CRS strings that pyproj cannot @@ -706,7 +702,7 @@ def open_geotiff(source: str | BinaryIO, *, ``UnparseableCRSError`` instead of landing in ``attrs['crs_wkt']`` verbatim. Set to ``True`` to keep the permissive behaviour where the citation field passes through unchanged. - Matches the same kwarg on ``to_geotiff`` / ``write_geotiff_gpu`` + Matches the same kwarg on ``to_geotiff`` / ``_write_geotiff_gpu`` so a value the reader accepted can survive a round-trip. allow_invalid_nodata : bool, default False [advanced] Read-side opt-in for integer-dtype sources whose @@ -793,8 +789,8 @@ def open_geotiff(source: str | BinaryIO, *, Safe VRT usage. Mosaic two compatible tiles and read with the fail-closed defaults: - >>> from xrspatial.geotiff import open_geotiff, write_vrt - >>> vrt_path = write_vrt( # doctest: +SKIP + >>> from xrspatial.geotiff import open_geotiff, build_vrt + >>> vrt_path = build_vrt( # doctest: +SKIP ... 'mosaic.vrt', ... source_files=['tile_west.tif', 'tile_east.tif'], ... ) @@ -818,7 +814,7 @@ def open_geotiff(source: str | BinaryIO, *, # All dispatcher-level kwarg rejection lives in # ``_validate_dispatch_kwargs`` so the three direct backends - # (``read_geotiff_dask``, ``read_geotiff_gpu``, ``read_vrt``) + # (``_read_geotiff_dask``, ``_read_geotiff_gpu``, ``_read_vrt``) # surface the same errors when called directly. The single call # runs ``_validate_overview_level_arg``, the ``on_gpu_failure`` # GPU-only guard, the ``missing_sources`` VRT-only guard, the @@ -850,7 +846,7 @@ def open_geotiff(source: str | BinaryIO, *, # VRT parse before it is refused. ``_validate_stable_only_remote`` # documents exactly this ordering contract ("before any range GET or # decode work"). ``_validate_stable_only_vrt`` is the matching gate - # for ``.vrt`` sources; ``read_vrt`` runs it again on the direct-call + # for ``.vrt`` sources; ``_read_vrt`` runs it again on the direct-call # path, so this is defence in depth, not the only gate. Each helper is # a no-op for the wrong source type, but branching keeps the intent # obvious. Running ahead of the bbox block also puts this gate ahead @@ -897,7 +893,7 @@ def open_geotiff(source: str | BinaryIO, *, # VRT files (string paths only -- VRT XML references other files on disk) if _is_vrt_source: - # ``read_vrt`` does not accept ``overview_level`` (the VRT XML + # ``_read_vrt`` does not accept ``overview_level`` (the VRT XML # references its own source files; overview selection would need # to apply to each one). Silently dropping the kwarg is the same # class of bug the dask and GPU dispatchers already guard against, @@ -916,8 +912,8 @@ def open_geotiff(source: str | BinaryIO, *, "overview_level is not supported for VRT sources. " "VRT references its own source files; pass overview_level " "to open_geotiff on a .tif source, or drop the kwarg.") - # ``on_gpu_failure`` only routes through ``read_geotiff_gpu``. - # ``read_vrt`` has no analogous failure policy, so any value the + # ``on_gpu_failure`` only routes through ``_read_geotiff_gpu``. + # ``_read_vrt`` has no analogous failure policy, so any value the # caller supplied alongside a VRT source would be silently lost. # The ``gpu=False`` branch is already rejected above; this catches # the ``gpu=True, source.endswith('.vrt')`` case the earlier check @@ -926,23 +922,23 @@ def open_geotiff(source: str | BinaryIO, *, raise ValueError( "on_gpu_failure is not supported for VRT sources. " "VRT reads do not go through the GPU decoder pipeline; " - "drop the kwarg or call read_geotiff_gpu directly on a " + "drop the kwarg or call _read_geotiff_gpu directly on a " ".tif source.") vrt_kwargs = {} if missing_sources_passed: vrt_kwargs['missing_sources'] = missing_sources - return read_vrt(source, dtype=dtype, window=window, band=band, - name=name, chunks=chunks, gpu=gpu, - max_pixels=max_pixels, - allow_rotated=allow_rotated, - allow_unparseable_crs=allow_unparseable_crs, - allow_invalid_nodata=allow_invalid_nodata, - stable_only=stable_only, - allow_experimental_codecs=allow_experimental_codecs, - allow_internal_only_jpeg=allow_internal_only_jpeg, - band_nodata=band_nodata, - mask_nodata=mask_nodata, - **vrt_kwargs) + return _read_vrt(source, dtype=dtype, window=window, band=band, + name=name, chunks=chunks, gpu=gpu, + max_pixels=max_pixels, + allow_rotated=allow_rotated, + allow_unparseable_crs=allow_unparseable_crs, + allow_invalid_nodata=allow_invalid_nodata, + stable_only=stable_only, + allow_experimental_codecs=allow_experimental_codecs, + allow_internal_only_jpeg=allow_internal_only_jpeg, + band_nodata=band_nodata, + mask_nodata=mask_nodata, + **vrt_kwargs) # File-like buffer rejections for ``gpu=True`` / ``chunks=...`` already # fired inside ``_validate_dispatch_kwargs`` above; the non-VRT branches @@ -954,37 +950,37 @@ def open_geotiff(source: str | BinaryIO, *, gpu_kwargs = {} if on_gpu_failure is not _ON_GPU_FAILURE_SENTINEL: gpu_kwargs['on_gpu_failure'] = on_gpu_failure - return read_geotiff_gpu(source, dtype=dtype, - overview_level=overview_level, - window=window, band=band, - name=name, chunks=chunks, - max_pixels=max_pixels, - allow_rotated=allow_rotated, - allow_unparseable_crs=allow_unparseable_crs, - allow_invalid_nodata=allow_invalid_nodata, - stable_only=stable_only, - allow_experimental_codecs=( - allow_experimental_codecs), - allow_internal_only_jpeg=( - allow_internal_only_jpeg), - mask_nodata=mask_nodata, - **gpu_kwargs) - - # Dask path (CPU) - if chunks is not None: - return read_geotiff_dask(source, dtype=dtype, chunks=chunks, + return _read_geotiff_gpu(source, dtype=dtype, overview_level=overview_level, window=window, band=band, - max_pixels=max_pixels, name=name, + name=name, chunks=chunks, + max_pixels=max_pixels, allow_rotated=allow_rotated, allow_unparseable_crs=allow_unparseable_crs, allow_invalid_nodata=allow_invalid_nodata, stable_only=stable_only, allow_experimental_codecs=( - allow_experimental_codecs), + allow_experimental_codecs), allow_internal_only_jpeg=( + allow_internal_only_jpeg), + mask_nodata=mask_nodata, + **gpu_kwargs) + + # Dask path (CPU) + if chunks is not None: + return _read_geotiff_dask(source, dtype=dtype, chunks=chunks, + overview_level=overview_level, + window=window, band=band, + max_pixels=max_pixels, name=name, + allow_rotated=allow_rotated, + allow_unparseable_crs=allow_unparseable_crs, + allow_invalid_nodata=allow_invalid_nodata, + stable_only=stable_only, + allow_experimental_codecs=( + allow_experimental_codecs), + allow_internal_only_jpeg=( allow_internal_only_jpeg), - mask_nodata=mask_nodata) + mask_nodata=mask_nodata) kwargs = {} if max_pixels is not None: @@ -995,7 +991,7 @@ def open_geotiff(source: str | BinaryIO, *, # ``read_to_array`` validates ``window`` against the selected IFD's # extent and raises ``ValueError`` for out-of-bounds windows with # the same message format as the dask path's pre-flight validator - # in :func:`read_geotiff_dask`. That keeps the two backends in sync + # in :func:`_read_geotiff_dask`. That keeps the two backends in sync # on the contract without forcing a second metadata parse here. arr, geo_info = _read_to_array( source, window=window, diff --git a/xrspatial/geotiff/_attrs.py b/xrspatial/geotiff/_attrs.py index de1185de0..8515960fb 100644 --- a/xrspatial/geotiff/_attrs.py +++ b/xrspatial/geotiff/_attrs.py @@ -479,7 +479,7 @@ def _validate_write_rich_tag_optin( if allow_experimental_codecs: return # Round-trip exemption: a DataArray that came from - # ``open_geotiff`` / ``read_geotiff_dask`` / ``read_geotiff_gpu`` + # ``open_geotiff`` / ``_read_geotiff_dask`` / ``_read_geotiff_gpu`` # carries the contract marker. Writing it back is the canonical # round-trip and should not require a new flag. # @@ -1216,7 +1216,7 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None ``window`` is a ``(r0, c0, r1, c1)`` tuple for windowed reads; when set, the emitted ``attrs['transform']`` shifts the origin to the window's top-left. The eager path and the dask path (which threads - ``window=`` through ``read_geotiff_dask``) both pass + ``window=`` through ``_read_geotiff_dask``) both pass the outer window through this helper so the resulting DataArray advertises the windowed transform. The GPU path does not currently expose a windowed read, so it passes ``window=None``. @@ -1400,7 +1400,7 @@ def _extract_rich_tags(attrs: dict) -> dict: """Extract the rich-tag set forwarded by the writers to ``write(...)``. Centralises the bookkeeping shared by :func:`to_geotiff`, - :func:`_write_vrt_tiled`, and :func:`write_geotiff_gpu`: + :func:`_write_vrt_tiled`, and :func:`_write_geotiff_gpu`: * ``raster_type`` -- mapped from ``attrs['raster_type']`` ('point' becomes :data:`RASTER_PIXEL_IS_POINT`; everything else stays diff --git a/xrspatial/geotiff/_backends/_gpu_helpers.py b/xrspatial/geotiff/_backends/_gpu_helpers.py index 5d1422358..32255f2c5 100644 --- a/xrspatial/geotiff/_backends/_gpu_helpers.py +++ b/xrspatial/geotiff/_backends/_gpu_helpers.py @@ -1,4 +1,4 @@ -"""GPU-only helpers consumed by ``read_geotiff_gpu`` and the GPU write path. +"""GPU-only helpers consumed by ``_read_geotiff_gpu`` and the GPU write path. Lazy-imports cupy at call time so the module imports cleanly on numpy-only environments. Every public entry point that touches GPU @@ -105,7 +105,7 @@ def _gpu_decode_single_band_tiles( invoke those kernels once per band with ``samples=1`` and stack the resulting 2-D arrays into ``(H, W, samples)`` afterwards. - Mirrors the two-stage GPU pipeline in ``read_geotiff_gpu`` -- GDS + Mirrors the two-stage GPU pipeline in ``_read_geotiff_gpu`` -- GDS first, then CPU-extracted-tiles GPU decode. ``lazy_data`` is a zero-arg callable that returns the full file bytes; it caches its result so the first band that needs the stage-2 fallback pays the @@ -135,7 +135,7 @@ def _gpu_decode_single_band_tiles( if gpu == 'strict' or _geotiff_strict_mode(): raise warnings.warn( - f"read_geotiff_gpu: GPU decode failed " + f"_read_geotiff_gpu: GPU decode failed " f"({type(e).__name__}: {e}); falling back to CPU.", RuntimeWarning, stacklevel=3, @@ -159,7 +159,7 @@ def _gpu_decode_single_band_tiles( if gpu == 'strict' or _geotiff_strict_mode(): raise warnings.warn( - f"read_geotiff_gpu: GPU decode failed " + f"_read_geotiff_gpu: GPU decode failed " f"({type(e).__name__}: {e}); falling back to CPU.", RuntimeWarning, stacklevel=3, @@ -225,7 +225,7 @@ def _apply_orientation_geo_info(geo_info, orientation: int, file_h: int, file_w: int): """Mirror the transform updates `_reader.read_to_array` does post-flip. - Centralised so both ``read_to_array`` (CPU) and ``read_geotiff_gpu`` + Centralised so both ``read_to_array`` (CPU) and ``_read_geotiff_gpu`` (this module) update the GeoTransform consistently. Operates only on ``geo_info.transform``; the rest of the GeoInfo struct stays as parsed. @@ -287,8 +287,8 @@ def _apply_orientation_geo_info(geo_info, orientation: int, def _gpu_apply_window_band(arr_gpu, geo_info, *, window, band): """Slice a fully-decoded GPU array down to a window and/or band. - Used by ``read_geotiff_gpu`` to keep the public surface in line with - ``open_geotiff`` and ``read_geotiff_dask``: callers can pass ``window`` + Used by ``_read_geotiff_gpu`` to keep the public surface in line with + ``open_geotiff`` and ``_read_geotiff_dask``: callers can pass ``window`` and ``band``, and the returned DataArray covers exactly that subset. The current implementation slices on device after the full-image GPU diff --git a/xrspatial/geotiff/_backends/dask.py b/xrspatial/geotiff/_backends/dask.py index 3de4d47a4..3cf48adad 100644 --- a/xrspatial/geotiff/_backends/dask.py +++ b/xrspatial/geotiff/_backends/dask.py @@ -1,9 +1,9 @@ -"""Dask read backend: ``read_geotiff_dask`` and ``_delayed_read_window``. +"""Dask read backend: ``_read_geotiff_dask`` and ``_delayed_read_window``. -``read_vrt`` is statically imported from the sibling ``.vrt`` module. +``_read_vrt`` is statically imported from the sibling ``.vrt`` module. ``_read_geo_info`` still lives in ``__init__.py`` and is lazy-imported -inside ``read_geotiff_dask``'s body to avoid a circular import -(``__init__.py`` re-exports ``read_geotiff_dask`` from here). +inside ``_read_geotiff_dask``'s body to avoid a circular import +(``__init__.py`` re-exports ``_read_geotiff_dask`` from here). """ from __future__ import annotations @@ -18,29 +18,29 @@ from .._reader import read_to_array as _read_to_array from .._runtime import _MISSING_SOURCES_SENTINEL, _ON_GPU_FAILURE_SENTINEL from .._validation import _validate_chunks_arg, _validate_dispatch_kwargs, _validate_dtype_cast -from .vrt import read_vrt - - -def read_geotiff_dask(source: str, *, - dtype: str | np.dtype | None = None, - window: tuple | None = None, - overview_level: int | None = None, - band: int | None = None, - name: str | None = None, - chunks: int | tuple = 512, - max_pixels: int | None = None, - max_cloud_bytes: int | None = ( +from .vrt import _read_vrt + + +def _read_geotiff_dask(source: str, *, + dtype: str | np.dtype | None = None, + window: tuple | None = None, + overview_level: int | None = None, + band: int | None = None, + name: str | None = None, + chunks: int | tuple = 512, + max_pixels: int | None = None, + max_cloud_bytes: int | None = ( _MAX_CLOUD_BYTES_SENTINEL), # type: ignore[assignment] - on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, - missing_sources: str = _MISSING_SOURCES_SENTINEL, - allow_rotated: bool = False, - allow_unparseable_crs: bool = False, - allow_invalid_nodata: bool = False, - stable_only: bool = False, - allow_experimental_codecs: bool = False, - allow_internal_only_jpeg: bool = False, - band_nodata: str | None = None, - mask_nodata: bool = True) -> xr.DataArray: + on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, + missing_sources: str = _MISSING_SOURCES_SENTINEL, + allow_rotated: bool = False, + allow_unparseable_crs: bool = False, + allow_invalid_nodata: bool = False, + stable_only: bool = False, + allow_experimental_codecs: bool = False, + allow_internal_only_jpeg: bool = False, + band_nodata: str | None = None, + mask_nodata: bool = True) -> xr.DataArray: """Read a GeoTIFF as a dask-backed DataArray for out-of-core processing. Release-contract tier (see @@ -95,7 +95,7 @@ def read_geotiff_dask(source: str, *, band_nodata : {'first', None}, optional [advanced] VRT-only opt-out for the fail-closed mixed-band-metadata check. Forwarded - verbatim to ``read_vrt`` when the source is a ``.vrt`` file. + verbatim to ``_read_vrt`` when the source is a ``.vrt`` file. Passing it with a non-VRT GeoTIFF source raises ``ValueError``. mask_nodata : bool, default True [stable] If True, replace the nodata sentinel with NaN per @@ -127,7 +127,7 @@ def read_geotiff_dask(source: str, *, ``open_geotiff`` for the full description. stable_only : bool, default False [advanced] Read-side opt-in that restricts the read to the - stable-tier local-file path. Forwarded to ``read_vrt`` when the + stable-tier local-file path. Forwarded to ``_read_vrt`` when the source ends in ``.vrt`` so the rejection fires at graph-build time. Advanced-tier sources (VRT, and HTTP / fsspec sources such as ``http(s)://`` or ``s3://``) are rejected; only a @@ -148,12 +148,12 @@ def read_geotiff_dask(source: str, *, on_gpu_failure : str, optional [internal-only] Accepted for cross-backend signature symmetry only. The dask path runs CPU decoders, so passing this kwarg - raises ``ValueError`` at dispatch. See ``read_geotiff_gpu`` for + raises ``ValueError`` at dispatch. See ``_read_geotiff_gpu`` for the kwarg's meaning on the GPU reader. missing_sources : {'raise', 'warn'}, optional - [advanced] VRT-only. Forwarded to ``read_vrt`` when the source + [advanced] VRT-only. Forwarded to ``_read_vrt`` when the source ends in ``.vrt``; otherwise raises ``ValueError`` at dispatch. - See ``read_vrt`` for the full description. + See ``_read_vrt`` for the full description. max_cloud_bytes : int or None, optional [internal-only] Accepted for cross-backend signature symmetry only. The dask reader uses bounded range GETs and does not @@ -196,7 +196,7 @@ def read_geotiff_dask(source: str, *, # Reject non-positive chunk sizes up front. ``chunks=0`` and negative # values otherwise propagate into dask chunk math (``range(0, N, 0)`` # ValueError, or empty chunk grids) with no indication that ``chunks`` - # was the problem. Shared with ``read_geotiff_gpu`` / ``read_vrt`` via + # was the problem. Shared with ``_read_geotiff_gpu`` / ``_read_vrt`` via # ``_validate_chunks_arg`` so all three entry points emit the same # error format. ``allow_none=False`` (the default) # rejects ``chunks=None`` with the same ValueError; this entry point @@ -204,16 +204,16 @@ def read_geotiff_dask(source: str, *, # would otherwise fail with a confusing TypeError. chunks = _validate_chunks_arg(chunks) - # ``open_geotiff`` already routes ``.vrt`` to ``read_vrt`` before - # reaching here, so this branch is only hit when ``read_geotiff_dask`` + # ``open_geotiff`` already routes ``.vrt`` to ``_read_vrt`` before + # reaching here, so this branch is only hit when ``_read_geotiff_dask`` # is called directly with a VRT path. Keep it as a defensive fallback # rather than letting the windowed-read path try to parse VRT XML as - # TIFF bytes. ``read_vrt`` is the single source of truth for VRT. + # TIFF bytes. ``_read_vrt`` is the single source of truth for VRT. if isinstance(source, str) and source.lower().endswith('.vrt'): vrt_kwargs = {} if missing_sources is not _MISSING_SOURCES_SENTINEL: vrt_kwargs['missing_sources'] = missing_sources - return read_vrt( + return _read_vrt( source, dtype=dtype, window=window, band=band, name=name, chunks=chunks, max_pixels=max_pixels, allow_rotated=allow_rotated, @@ -228,10 +228,10 @@ def read_geotiff_dask(source: str, *, ) # ``open_geotiff`` gates ``stable_only=True`` for remote sources before - # dispatching here, but ``read_geotiff_dask`` is also a direct entry + # dispatching here, but ``_read_geotiff_dask`` is also a direct entry # point. Apply the same gate so a direct caller cannot read an # advanced-tier HTTP / fsspec source under ``stable_only=True``. The VRT - # branch above already forwards ``stable_only`` to ``read_vrt``. + # branch above already forwards ``stable_only`` to ``_read_vrt``. from .._validation import _validate_stable_only_remote _validate_stable_only_remote( source, @@ -354,7 +354,7 @@ def read_geotiff_dask(source: str, *, geo_info._ifd_compression = http_ifd.compression else: # Metadata-only read: O(1) memory via mmap, no pixel decompression. - # Lazy import for the same circular-import reason as ``read_vrt`` + # Lazy import for the same circular-import reason as ``_read_vrt`` # above: ``_read_geo_info`` still lives in ``xrspatial.geotiff``. from .. import _read_geo_info geo_info, full_h, full_w, file_dtype, n_bands = _read_geo_info( @@ -380,7 +380,7 @@ def read_geotiff_dask(source: str, *, _compression_tag, allow_experimental_codecs=allow_experimental_codecs, allow_internal_only_jpeg=allow_internal_only_jpeg, - entry_point="read_geotiff_dask", + entry_point="_read_geotiff_dask", ) # Centralize the nodata lifecycle in one value object. @@ -532,7 +532,7 @@ def read_geotiff_dask(source: str, *, suggested_h = int(math.ceil(ch_h * scale)) suggested_w = int(math.ceil(ch_w * scale)) raise ValueError( - f"read_geotiff_dask: chunks=({ch_h}, {ch_w}) on a " + f"_read_geotiff_dask: chunks=({ch_h}, {ch_w}) on a " f"{full_h}x{full_w} image would produce {n_chunks:,} dask " f"tasks, exceeding the {_MAX_DASK_CHUNKS:,}-task cap. Pass a " f"larger chunks=... value explicitly (e.g. chunks=" @@ -633,7 +633,7 @@ def _delayed_read_window(source, r0, c0, r1, c1, overview_level, nodata, """Dask-delayed function to read a single window. *http_meta_key* is an optional ``Delayed[(TIFFHeader, IFD)]`` parsed - once by :func:`read_geotiff_dask` and wrapped via ``dask.delayed``. + once by :func:`_read_geotiff_dask` and wrapped via ``dask.delayed``. Passing it as a function argument (rather than a closure capture) makes the metadata a single graph input that all window tasks depend on, so distributed/process schedulers serialise it once diff --git a/xrspatial/geotiff/_backends/gpu.py b/xrspatial/geotiff/_backends/gpu.py index 616a27f74..d229271c2 100644 --- a/xrspatial/geotiff/_backends/gpu.py +++ b/xrspatial/geotiff/_backends/gpu.py @@ -1,8 +1,8 @@ -"""GPU read backend: ``read_geotiff_gpu`` and its private helpers. +"""GPU read backend: ``_read_geotiff_gpu`` and its private helpers. -``_read_geotiff_gpu_chunked`` calls into ``read_geotiff_dask`` for its +``_read_geotiff_gpu_chunked`` calls into ``_read_geotiff_dask`` for its CPU-decode fallback path, imported statically at the top of the module -via ``from .dask import read_geotiff_dask``. +via ``from .dask import _read_geotiff_dask``. """ from __future__ import annotations @@ -24,7 +24,7 @@ from ._gpu_helpers import (_apply_nodata_mask_gpu, _apply_orientation_geo_info, _apply_orientation_gpu, _gpu_apply_window_band, _gpu_decode_single_band_tiles) -from .dask import read_geotiff_dask +from .dask import _read_geotiff_dask def _preflight_cuda_runtime(cupy) -> None: @@ -42,39 +42,39 @@ def _preflight_cuda_runtime(cupy) -> None: device_count = cupy.cuda.runtime.getDeviceCount() except Exception as e: raise RuntimeError( - f"read_geotiff_gpu: CUDA runtime is not usable " + f"_read_geotiff_gpu: CUDA runtime is not usable " f"({type(e).__name__}: {e}). Check the GPU driver matches " f"the installed cupy build, or pass gpu=False." ) from e if device_count == 0: raise RuntimeError( - "read_geotiff_gpu: cupy reports 0 CUDA devices. Check " + "_read_geotiff_gpu: cupy reports 0 CUDA devices. Check " "the GPU driver and CUDA_VISIBLE_DEVICES, or pass gpu=False." ) -def read_geotiff_gpu(source: str, *, - dtype: str | np.dtype | None = None, - window: tuple | None = None, - overview_level: int | None = None, - band: int | None = None, - name: str | None = None, - chunks: int | tuple | None = None, - max_pixels: int | None = None, - max_cloud_bytes: int | None = ( +def _read_geotiff_gpu(source: str, *, + dtype: str | np.dtype | None = None, + window: tuple | None = None, + overview_level: int | None = None, + band: int | None = None, + name: str | None = None, + chunks: int | tuple | None = None, + max_pixels: int | None = None, + max_cloud_bytes: int | None = ( _MAX_CLOUD_BYTES_SENTINEL), # type: ignore[assignment] - on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, - missing_sources: str = _MISSING_SOURCES_SENTINEL, - allow_rotated: bool = False, - allow_unparseable_crs: bool = False, - allow_invalid_nodata: bool = False, - stable_only: bool = False, - allow_experimental_codecs: bool = False, - allow_internal_only_jpeg: bool = False, - band_nodata: str | None = None, - mask_nodata: bool = True, - gpu: str = _GPU_DEPRECATED_SENTINEL, - ) -> xr.DataArray: + on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, + missing_sources: str = _MISSING_SOURCES_SENTINEL, + allow_rotated: bool = False, + allow_unparseable_crs: bool = False, + allow_invalid_nodata: bool = False, + stable_only: bool = False, + allow_experimental_codecs: bool = False, + allow_internal_only_jpeg: bool = False, + band_nodata: str | None = None, + mask_nodata: bool = True, + gpu: str = _GPU_DEPRECATED_SENTINEL, + ) -> xr.DataArray: """Read a GeoTIFF with GPU-accelerated decompression via Numba CUDA. Release-contract tier (see @@ -113,7 +113,7 @@ def read_geotiff_gpu(source: str, *, dtype : str, numpy.dtype, or None [experimental] Cast the result to this dtype after reading. None keeps the file's native dtype. Float-to-int casts raise - ValueError, mirroring ``open_geotiff`` / ``read_geotiff_dask``. + ValueError, mirroring ``open_geotiff`` / ``_read_geotiff_dask``. overview_level : int or None [experimental] Overview level (0 = full resolution). window : tuple or None @@ -121,7 +121,7 @@ def read_geotiff_gpu(source: str, *, for windowed reading. None reads the full raster. The GPU pipeline currently decodes all tiles and slices on device after assembly, so the kwarg restores API parity with - ``open_geotiff`` and ``read_geotiff_dask`` but does not yet + ``open_geotiff`` and ``_read_geotiff_dask`` but does not yet skip I/O for partial windows. The returned coords, ``attrs['transform']``, and shape match the eager numpy path. band : int or None @@ -179,7 +179,7 @@ def read_geotiff_gpu(source: str, *, ``on_gpu_failure`` raises ``TypeError``. The old name shipped with values ``'auto'`` / ``'strict'`` and was easy to confuse with the boolean ``gpu=`` kwarg on ``open_geotiff`` / - ``to_geotiff`` / ``read_vrt``. + ``to_geotiff`` / ``_read_vrt``. mask_nodata : bool, default True [experimental] If True, replace the nodata sentinel with NaN (integer rasters get promoted to ``float64`` first). If False, @@ -214,7 +214,7 @@ def read_geotiff_gpu(source: str, *, fsspec sources through the CPU fallback, and those advanced-tier readers must be gated. With ``stable_only=True`` a remote source raises ``RemoteStableSourcesOnlyError`` before any cupy import or - decode, matching ``open_geotiff`` and ``read_geotiff_dask``. Pass + decode, matching ``open_geotiff`` and ``_read_geotiff_dask``. Pass ``allow_experimental_codecs=True`` to unlock the advanced tier. See ``open_geotiff`` for the full description. allow_experimental_codecs : bool, default False @@ -229,14 +229,14 @@ def read_geotiff_gpu(source: str, *, for the full description. band_nodata : {'first', None}, optional [internal-only] VRT-only. Accepted at the signature level for - parity with ``open_geotiff``; passing it to ``read_geotiff_gpu`` + parity with ``open_geotiff``; passing it to ``_read_geotiff_gpu`` raises ``ValueError`` because the GPU dispatcher rejects ``.vrt`` sources up front and the kwarg only applies to VRT. - See ``read_vrt`` for the kwarg's meaning. + See ``_read_vrt`` for the kwarg's meaning. missing_sources : {'raise', 'warn'}, optional [internal-only] VRT-only. Same shape as ``band_nodata`` above: accepted for signature parity, rejected at dispatch with - ``ValueError`` for non-VRT sources. See ``read_vrt`` for the + ``ValueError`` for non-VRT sources. See ``_read_vrt`` for the full description. max_cloud_bytes : int or None, optional [internal-only] Accepted for cross-backend signature symmetry @@ -278,7 +278,7 @@ def read_geotiff_gpu(source: str, *, max_cloud_bytes=max_cloud_bytes, ) - # ``open_geotiff`` and ``read_geotiff_dask`` gate ``stable_only=True`` + # ``open_geotiff`` and ``_read_geotiff_dask`` gate ``stable_only=True`` # for advanced-tier HTTP / fsspec sources before dispatching. This GPU # entry point is also a direct public reader and it routes remote # sources through the CPU fallback below, so a direct caller could read @@ -300,13 +300,13 @@ def read_geotiff_gpu(source: str, *, # chosen (including the matching ``on_gpu_failure='auto', # gpu='auto'`` pair). Refuse rather than silently picking one. raise TypeError( - "read_geotiff_gpu: pass either 'on_gpu_failure' or the " + "_read_geotiff_gpu: pass either 'on_gpu_failure' or the " "deprecated 'gpu' alias, not both.") if old_passed: warnings.warn( - "read_geotiff_gpu(..., gpu=...) is deprecated; use " + "_read_geotiff_gpu(..., gpu=...) is deprecated; use " "on_gpu_failure=... instead. The kwarg was renamed because " - "'gpu' on open_geotiff/to_geotiff/read_vrt is a bool that " + "'gpu' on open_geotiff/to_geotiff/_read_vrt is a bool that " "selects the GPU backend, while here it selects the failure " "policy when the GPU path raises.", DeprecationWarning, @@ -320,7 +320,7 @@ def read_geotiff_gpu(source: str, *, raise ValueError( f"on_gpu_failure must be 'auto' or 'strict', got {gpu!r}") # Reject non-positive chunk sizes up front so the GPU dask+cupy path - # surfaces the same error as ``read_geotiff_dask``. Previously + # surfaces the same error as ``_read_geotiff_dask``. Previously # ``chunks=0`` raised ``ZeroDivisionError`` deep in cupy/dask, and # ``chunks=-1`` was silently accepted (negative chunks fall out of # the dask chunk grid as a no-op). ``chunks=None`` is the default @@ -484,7 +484,7 @@ def read_geotiff_gpu(source: str, *, ifd.compression, allow_experimental_codecs=allow_experimental_codecs, allow_internal_only_jpeg=allow_internal_only_jpeg, - entry_point="read_geotiff_gpu", + entry_point="_read_geotiff_gpu", ) # Keep ``data`` / ``header`` bound to the base file's buffers so @@ -531,7 +531,7 @@ def read_geotiff_gpu(source: str, *, # file pixels or display pixels?), so the CPU reader # ``_reader.read_to_array`` rejects ``window=`` for orientation != 1. # Mirror that here so the GPU path agrees with the CPU path and - # ``read_geotiff_dask``. Use the same error wording so the failure + # ``_read_geotiff_dask``. Use the same error wording so the failure # message is identical across backends. if orientation != 1 and window is not None: raise ValueError( @@ -544,7 +544,7 @@ def read_geotiff_gpu(source: str, *, # Validate band against the selected IFD's sample count. # ``samples_per_pixel`` is at least 1 for any valid TIFF; we treat # ``band=0`` as "first band" for single-band files too so the - # behaviour mirrors ``read_geotiff_dask``. + # behaviour mirrors ``_read_geotiff_dask``. ifd_samples = ifd.samples_per_pixel if band is not None: # Reject ``bool`` and ``np.bool_`` up front; @@ -859,7 +859,7 @@ def _read_once(): if gpu == 'strict' or _geotiff_strict_mode(): raise warnings.warn( - f"read_geotiff_gpu: GPU decode failed " + f"_read_geotiff_gpu: GPU decode failed " f"({type(e).__name__}: {e}); falling back to CPU.", RuntimeWarning, stacklevel=2, @@ -898,7 +898,7 @@ def _read_once(): if gpu == 'strict' or _geotiff_strict_mode(): raise warnings.warn( - f"read_geotiff_gpu: GPU decode failed " + f"_read_geotiff_gpu: GPU decode failed " f"({type(e).__name__}: {e}); falling back to CPU.", RuntimeWarning, stacklevel=2, @@ -1076,7 +1076,7 @@ def _read_geotiff_gpu_eager_via_cpu(source, *, dtype, window, overview_level, """Eager CPU decode + GPU upload for HTTP / fsspec sources. Reached via ``open_geotiff(url, gpu=True)`` and the direct - ``read_geotiff_gpu(url)`` entry point. The eager GPU pipeline + ``_read_geotiff_gpu(url)`` entry point. The eager GPU pipeline assumes the source is a local file: it opens ``_FileSource(source)``, calls ``gpu_decode_tiles_from_file`` (which KvikIO-DMAs the on-disk tiles), and falls back to a local ``mmap`` slice in the CPU @@ -1304,7 +1304,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level, allow_experimental_codecs: bool = False, allow_internal_only_jpeg: bool = False, mask_nodata: bool = True): - """Lazy Dask+CuPy backend for ``read_geotiff_gpu(chunks=...)``. + """Lazy Dask+CuPy backend for ``_read_geotiff_gpu(chunks=...)``. Two paths produce the same shape of dask graph: @@ -1317,7 +1317,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level, 2. **CPU decode + GPU upload** for everything else (HTTP / fsspec, no KvikIO, planar=2, sparse, MinIsWhite, non-trivial orientation, - stripped layouts). Reuses ``read_geotiff_dask`` to build the + stripped layouts). Reuses ``_read_geotiff_dask`` to build the per-chunk windowed delayed graph and ``map_blocks(cupy.asarray)`` to upload each block. Peak GPU memory is still one chunk; the cost is per-chunk CPU decode rather than GDS DMA. @@ -1337,7 +1337,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level, # the GDS fast # path (handled in ``_read_geotiff_gpu_chunked_gds`` which runs # the same cap on its own metadata parse) or falls through to - # ``read_geotiff_dask`` whose per-chunk ``read_to_array`` calls + # ``_read_geotiff_dask`` whose per-chunk ``read_to_array`` calls # apply the cap inside the CPU reader. The check here closes the # window between "qualification probe parses the IFDs" and "the # dispatch decides which path to take" so a forged tile is @@ -1387,7 +1387,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level, ifd = select_overview_ifd(ifds, overview_level) # The GDS qualification probe parses base-file IFDs only -- # sidecar files do not qualify for the disk->GPU fast path - # and the chunked path falls through to ``read_geotiff_dask`` + # and the chunked path falls through to ``_read_geotiff_dask`` # which carries its own sidecar handling. Pass # ``sidecar_origin=None`` explicitly so all four call sites # of this helper share the same call shape. @@ -1419,7 +1419,7 @@ def _read_geotiff_gpu_chunked(source, *, dtype, chunks, overview_level, # for (the CPU path re-parses metadata anyway). pass - cpu_da = read_geotiff_dask( + cpu_da = _read_geotiff_dask( source, dtype=dtype, chunks=chunks, overview_level=overview_level, window=window, band=band, max_pixels=max_pixels, name=name, @@ -1662,7 +1662,7 @@ def _chunk_task(meta, r0, c0, r1, c1): blocks_rows.append(da_mod.concatenate(blocks_cols, axis=1)) dask_arr = da_mod.concatenate(blocks_rows, axis=0) - # Build coords/attrs that match read_geotiff_dask's output. + # Build coords/attrs that match _read_geotiff_dask's output. coords = _coords_from_geo_info(geo_info, out_h, out_w, window=window) if out_has_band_axis: dims = ['y', 'x', 'band'] diff --git a/xrspatial/geotiff/_backends/vrt.py b/xrspatial/geotiff/_backends/vrt.py index 344fe0720..023ce3482 100644 --- a/xrspatial/geotiff/_backends/vrt.py +++ b/xrspatial/geotiff/_backends/vrt.py @@ -1,9 +1,9 @@ -"""VRT read backend: ``read_vrt`` and its dask helpers. +"""VRT read backend: ``_read_vrt`` and its dask helpers. The XML parsing, source-path containment, per-source decode, and integer-sentinel masking live in ``xrspatial/geotiff/_vrt.py``; this module holds only the orchestration that picks among the eager, dask, -and GPU paths exposed through the public ``read_vrt`` entry point. +and GPU paths exposed through the public ``_read_vrt`` entry point. """ from __future__ import annotations @@ -22,7 +22,7 @@ _validate_dispatch_kwargs, _validate_dtype_cast) # Hard cap on the per-VRT chunk task count. Matches the -# ``_MAX_DASK_CHUNKS`` value used by ``read_geotiff_dask`` so the two +# ``_MAX_DASK_CHUNKS`` value used by ``_read_geotiff_dask`` so the two # entry points refuse the same scheduler-busting chunk grids. _MAX_VRT_DASK_CHUNKS = 50_000 @@ -112,26 +112,26 @@ def _vrt_to_synthetic_geo_info(vrt) -> GeoInfo: ) -def read_vrt(source: str, *, - dtype: str | np.dtype | None = 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, - max_cloud_bytes: int | None = _MAX_CLOUD_BYTES_SENTINEL, # type: ignore[assignment] - on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, - missing_sources: str = 'raise', - allow_rotated: bool = False, - allow_unparseable_crs: bool = False, - allow_invalid_nodata: bool = False, - stable_only: bool = False, - allow_experimental_codecs: bool = False, - allow_internal_only_jpeg: bool = False, - band_nodata: str | None = None, - mask_nodata: bool = True) -> xr.DataArray: +def _read_vrt(source: str, *, + dtype: str | np.dtype | None = 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, + max_cloud_bytes: int | None = _MAX_CLOUD_BYTES_SENTINEL, # type: ignore[assignment] + on_gpu_failure: str = _ON_GPU_FAILURE_SENTINEL, + missing_sources: str = 'raise', + allow_rotated: bool = False, + allow_unparseable_crs: bool = False, + allow_invalid_nodata: bool = False, + stable_only: bool = False, + allow_experimental_codecs: bool = False, + allow_internal_only_jpeg: bool = False, + band_nodata: str | None = None, + mask_nodata: bool = True) -> xr.DataArray: """Read a GDAL Virtual Raster Table (.vrt) into an xarray.DataArray. Release-contract tier (see @@ -202,13 +202,13 @@ def read_vrt(source: str, *, [advanced] Maximum allowed pixel count (width * height * samples) for the assembled VRT region. None uses the reader default (~1 billion). Matches ``open_geotiff`` - / ``read_geotiff_dask`` / ``read_geotiff_gpu``. + / ``_read_geotiff_dask`` / ``_read_geotiff_gpu``. missing_sources : {'raise', 'warn'}, default 'raise' [advanced] Policy for unreadable source files referenced by the VRT. ``'raise'`` (the default) fails immediately on an unreadable backing source so a partial mosaic never surfaces - silently. This matches the internal ``_vrt.read_vrt`` default + silently. This matches the internal ``_vrt._read_vrt`` default and the rest of the geotiff module's up-front rejection of malformed input. Both the eager and chunked dispatchers raise at construction time when the static missing-source sweep @@ -262,7 +262,7 @@ def read_vrt(source: str, *, ``open_geotiff`` for the full description. stable_only : bool, default False [advanced] Read-side opt-in for stable-tier sources only. When - ``True``, ``read_vrt`` raises :class:`VRTStableSourcesOnlyError` + ``True``, ``_read_vrt`` raises :class:`VRTStableSourcesOnlyError` before any pixel decode because ``reader.vrt`` itself sits at the ``advanced`` tier in :data:`SUPPORTED_FEATURES` and VRT child sources can declare any codec the GeoTIFF reader supports @@ -290,7 +290,7 @@ def read_vrt(source: str, *, [internal-only] Accepted for cross-backend signature symmetry only. VRT reads do not go through the GPU decoder pipeline, so passing this kwarg raises ``ValueError`` at dispatch. See - ``read_geotiff_gpu`` for the kwarg's meaning on the GPU + ``_read_geotiff_gpu`` for the kwarg's meaning on the GPU reader. max_cloud_bytes : int or None, optional [internal-only] Accepted for cross-backend signature symmetry @@ -341,12 +341,12 @@ def read_vrt(source: str, *, Safe usage. Mosaic two compatible tiles and read with the fail-closed defaults: - >>> from xrspatial.geotiff import open_geotiff, write_vrt - >>> vrt_path = write_vrt( # doctest: +SKIP + >>> from xrspatial.geotiff import open_geotiff, build_vrt + >>> vrt_path = build_vrt( # doctest: +SKIP ... 'mosaic.vrt', ... source_files=['tile_west.tif', 'tile_east.tif'], ... ) - >>> da = read_vrt(vrt_path) # doctest: +SKIP + >>> da = _read_vrt(vrt_path) # doctest: +SKIP Intentionally raises. A VRT whose source tiles disagree on their per-band nodata sentinels is rejected by the default @@ -354,7 +354,7 @@ def read_vrt(source: str, *, >>> from xrspatial.geotiff import MixedBandMetadataError >>> try: # doctest: +SKIP - ... read_vrt('mixed_nodata.vrt') + ... _read_vrt('mixed_nodata.vrt') ... except MixedBandMetadataError: ... pass # pass band_nodata='first' to opt back into the ... # legacy flatten-to-band-0 semantics, or fix the @@ -385,14 +385,14 @@ def read_vrt(source: str, *, # Shared dispatcher-kwarg validator so direct callers see the same # rejections as ``open_geotiff``. For - # ``read_vrt`` the helper rejects ``on_gpu_failure`` (VRT reads do + # ``_read_vrt`` the helper rejects ``on_gpu_failure`` (VRT reads do # not go through a GPU decoder pipeline), ``max_cloud_bytes`` (the # VRT reader does not consume the cloud-byte budget), # and validates ``overview_level``'s type. ``missing_sources`` and # ``band_nodata`` are legitimate VRT kwargs so the helper's # VRT-only guard is a no-op here. ``gpu=False`` is passed so that # an explicit ``on_gpu_failure`` is rejected regardless of the - # ``read_vrt(gpu=)`` output-device kwarg. + # ``_read_vrt(gpu=)`` output-device kwarg. _validate_dispatch_kwargs( source=source, gpu=False, @@ -404,7 +404,7 @@ def read_vrt(source: str, *, max_cloud_bytes=max_cloud_bytes, ) - # ``overview_level`` is not consumed by ``read_vrt`` (the VRT XML + # ``overview_level`` is not consumed by ``_read_vrt`` (the VRT XML # references its own source files; overview selection would need to # apply to each one). ``overview_level=0`` matches the documented # "full resolution" default, so treat it as a no-op. Mirrors the @@ -419,7 +419,7 @@ def read_vrt(source: str, *, "to open_geotiff on a .tif source, or drop the kwarg.") # Reject non-positive chunk sizes up front so the VRT dask path - # surfaces the same error as ``read_geotiff_dask``. Without + # surfaces the same error as ``_read_geotiff_dask``. Without # this check ``chunks=0`` raised ``ZeroDivisionError`` deep in dask # and ``chunks=-1`` was silently accepted. ``chunks=None`` is the # default (eager read), so allow it through here. @@ -614,7 +614,7 @@ def read_vrt(source: str, *, # sentinel, the integer-promotion block below would mask against # band 0's sentinel, and band N's actual nodata pixels would # survive as literal integers. ``band`` has - # already been validated by ``_vrt.read_vrt`` as + # already been validated by ``_vrt._read_vrt`` as # 0 <= band < len(vrt.bands), so a simple lookup is safe here. # # Documented divergence: per-band sentinel selection cannot ride @@ -774,7 +774,7 @@ def _vrt_chunk_read(source, r0, c0, r1, c1, *, Called by ``dask.delayed`` from :func:`_read_vrt_chunked`. The function reads only the destination window via the existing VRT internal reader, applies the same integer-sentinel masking the - eager :func:`read_vrt` does post-decode, casts to the dtype the + eager :func:`_read_vrt` does post-decode, casts to the dtype the dask graph declared up front, and optionally moves the block to the GPU. @@ -790,7 +790,7 @@ def _vrt_chunk_read(source, r0, c0, r1, c1, *, ``allow_rotated`` / ``allow_invalid_nodata`` are forwarded to the internal reader so the per-source GeoTIFF read in each task honors - the opt-ins the caller set on the public ``read_vrt`` boundary, + the opt-ins the caller set on the public ``_read_vrt`` boundary, matching the eager path. """ from .._vrt import _apply_integer_sentinel_mask @@ -842,7 +842,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, allow_internal_only_jpeg: bool = False, band_nodata: str | None = None, mask_nodata: bool = True): - """Lazy ``read_vrt`` dispatch when ``chunks=`` is set. + """Lazy ``_read_vrt`` dispatch when ``chunks=`` is set. Parses the VRT XML once to recover the extent, CRS, GeoTransform, and per-band metadata, then builds a dask graph with one task per @@ -895,7 +895,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, # Centralised VRT capability validator. Run at graph # build time so capability mismatches surface here, not inside a - # per-chunk decode task. ``read_vrt(..., chunks=)`` previously let + # per-chunk decode task. ``_read_vrt(..., chunks=)`` previously let # unsupported features ride through the graph build and raised # deep in a ``compute()`` chunk function (an opaque user # experience); the validator moves the rejection back to where the @@ -952,8 +952,8 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, ) # Up-front pixel-count guard against the windowed extent. Mirrors - # the eager ``_vrt.read_vrt`` (which calls ``_check_dimensions`` on - # the full output shape) and ``read_geotiff_dask`` (which guards + # the eager ``_vrt._read_vrt`` (which calls ``_check_dimensions`` on + # the full output shape) and ``_read_geotiff_dask`` (which guards # ``full_h * full_w * eff_bands`` before scheduling any task). Each # chunk task additionally re-checks via ``max_pixels`` through the # internal reader, but catching an oversized request up front saves @@ -970,7 +970,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, ch_h, ch_w = chunks # Refuse chunk grids that would build more tasks than the scheduler - # can hold without OOMing the driver. ``read_geotiff_dask`` uses the + # can hold without OOMing the driver. ``_read_geotiff_dask`` uses the # same cap with the same suggestion logic (see the # ``_MAX_DASK_CHUNKS`` guard upstream). n_chunks = ((full_h + ch_h - 1) // ch_h) * ((full_w + ch_w - 1) // ch_w) @@ -979,7 +979,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, suggested_h = int(math.ceil(ch_h * scale)) suggested_w = int(math.ceil(ch_w * scale)) raise ValueError( - f"read_vrt: chunks=({ch_h}, {ch_w}) on a " + f"_read_vrt: chunks=({ch_h}, {ch_w}) on a " f"{full_h}x{full_w} VRT region would produce {n_chunks:,} " f"dask tasks, exceeding the {_MAX_VRT_DASK_CHUNKS:,}-task " f"cap. Pass a larger chunks=... value explicitly (e.g. " @@ -1107,7 +1107,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, final_dtype = declared_dtype # Coordinates: derive from the VRT GeoTransform and the windowed - # extent. Mirrors the eager branch in ``read_vrt`` so chunked and + # extent. Mirrors the eager branch in ``_read_vrt`` so chunked and # eager reads share the same x/y arrays. gt = vrt.geo_transform _vrt_is_rotated = ( @@ -1212,7 +1212,7 @@ def _read_vrt_chunked(source, *, window, band, name, chunks, gpu, dtype, }) # Fail-fast for ``missing_sources='raise'`` (the public default). - # The docstring at the top of ``read_vrt`` promises that + # The docstring at the top of ``_read_vrt`` promises that # ``'raise'`` "fails immediately on an unreadable backing source so a # partial mosaic never surfaces silently". Without this guard the # chunked path constructs a delayed graph whose tasks each raise diff --git a/xrspatial/geotiff/_cog_http.py b/xrspatial/geotiff/_cog_http.py index 71df66f15..97fa584d8 100644 --- a/xrspatial/geotiff/_cog_http.py +++ b/xrspatial/geotiff/_cog_http.py @@ -2,7 +2,7 @@ The helpers stay private to :mod:`xrspatial.geotiff`; public callers go through :func:`xrspatial.geotiff.open_geotiff` / -:func:`xrspatial.geotiff.read_geotiff_dask`. +:func:`xrspatial.geotiff._read_geotiff_dask`. Monkeypatch contract -------------------- @@ -111,7 +111,7 @@ def _parse_cog_http_meta( (lazy per-IFD reads); the cap exists to bound a malformed-file blast radius rather than to constrain valid pyramids. - Pulled out of :func:`_read_cog_http` so :func:`read_geotiff_dask` + Pulled out of :func:`_read_cog_http` so :func:`_read_geotiff_dask` can parse metadata once per graph rather than once per chunk task (each delayed task used to fire its own 16 KB header GET). @@ -778,7 +778,7 @@ def _fetch_decode_cog_http_tiles( """Fetch and decode the tiles of a tiled COG over HTTP. Pulled out of :func:`_read_cog_http` so that callers with - pre-parsed metadata (notably :func:`read_geotiff_dask`) can reuse a + pre-parsed metadata (notably :func:`_read_geotiff_dask`) can reuse a single IFD parse across many tile-fetch calls. When *window* is given, only tiles intersecting the window are fetched + decoded; the result is sized to the (clamped) window rather than the full diff --git a/xrspatial/geotiff/_coords.py b/xrspatial/geotiff/_coords.py index 4ea5ef1ad..e6c506d87 100644 --- a/xrspatial/geotiff/_coords.py +++ b/xrspatial/geotiff/_coords.py @@ -99,7 +99,7 @@ class GeorefResolution: applied_no_georef_marker: bool = False -# Names of dims that ``to_geotiff`` / ``write_geotiff_gpu`` treat as the +# Names of dims that ``to_geotiff`` / ``_write_geotiff_gpu`` treat as the # non-spatial band axis. Used both to remap ``(band, y, x)`` inputs to # ``(y, x, band)`` before writing and to skip the band axis when inferring # a GeoTransform from coords (see :func:`coords_to_transform`). @@ -145,7 +145,7 @@ def _has_no_georef_marker(da: Any) -> bool: should not be treated as truthy and silently drop a transform. Inputs that are not xarray DataArrays (e.g. a raw ``numpy.ndarray`` - or ``cupy.ndarray`` passed directly to ``write_geotiff_gpu``) carry + or ``cupy.ndarray`` passed directly to ``_write_geotiff_gpu``) carry no attrs and therefore no marker; return ``False`` rather than raise so callers can use this as a plain predicate. """ diff --git a/xrspatial/geotiff/_crs.py b/xrspatial/geotiff/_crs.py index dcf25d619..db912e1b1 100644 --- a/xrspatial/geotiff/_crs.py +++ b/xrspatial/geotiff/_crs.py @@ -3,7 +3,7 @@ ``_wkt_to_epsg`` and ``_resolve_crs_to_wkt`` are pure leaves over ``pyproj`` (lazy-imported inside) and the strict-mode / fallback-warning machinery from ``_runtime``. They are called from ``to_geotiff``, -``write_geotiff_gpu``, and ``write_vrt`` to normalise the EPSG / WKT / +``_write_geotiff_gpu``, and ``build_vrt`` to normalise the EPSG / WKT / PROJ kwarg they each accept. """ from __future__ import annotations @@ -228,13 +228,13 @@ def _validate_crs_fallback( def _resolve_crs_to_wkt(crs) -> str | None: """Normalise a CRS argument to a WKT string for downstream writers. - Mirrors ``to_geotiff`` / ``write_geotiff_gpu``'s ``crs`` kwarg semantics + Mirrors ``to_geotiff`` / ``_write_geotiff_gpu``'s ``crs`` kwarg semantics so callers can pass an int EPSG code, a WKT string, or a PROJ string interchangeably. Returns the canonical WKT string (or ``None`` if - ``crs`` is ``None``) for forwarding to ``_vrt.write_vrt``, which only + ``crs`` is ``None``) for forwarding to ``_vrt.build_vrt``, which only speaks WKT. - Used by ``write_vrt`` to close the parameter-naming + Used by ``build_vrt`` to close the parameter-naming drift versus the eager and GPU writer entry points. Parameters @@ -275,7 +275,7 @@ def _resolve_crs_to_wkt(crs) -> str | None: f"got {type(crs).__name__}") if isinstance(crs, str): # Empty string is a common "no CRS" sentinel from upstream - # GeoTIFFs; preserve the existing _vrt.write_vrt semantics (it + # GeoTIFFs; preserve the existing _vrt.build_vrt semantics (it # falls back to the first source's CRS for empty strings too). if not crs: return None diff --git a/xrspatial/geotiff/_decode.py b/xrspatial/geotiff/_decode.py index f7b7d1e09..0d370a972 100644 --- a/xrspatial/geotiff/_decode.py +++ b/xrspatial/geotiff/_decode.py @@ -572,7 +572,7 @@ def _read_tiles(data: bytes, ifd: IFD, header: TIFFHeader, # TIFF header against malformed values; it is not the caller's output # budget. The output-window check below uses ``max_pixels`` and is # what enforces the user's per-call memory cap. The source-read path - # under ``read_vrt`` relies on that output check to honour a small + # under ``_read_vrt`` relies on that output check to honour a small # caller ``max_pixels`` against a normal-tile source. _check_dimensions(tw, th, samples, MAX_PIXELS_DEFAULT, dtype=dtype) diff --git a/xrspatial/geotiff/_gpu_decode.py b/xrspatial/geotiff/_gpu_decode.py index ac7d57727..c2b50f847 100644 --- a/xrspatial/geotiff/_gpu_decode.py +++ b/xrspatial/geotiff/_gpu_decode.py @@ -78,7 +78,7 @@ def _check_gpu_memory(required_bytes: int, what: str = "tile buffer") -> None: f"but only {free:,} bytes free on device (cap is " f"{_GPU_FREE_MEMORY_FRACTION:.0%} of free = {budget:,} " "bytes). Consider reading the file in chunks via " - "read_geotiff_dask(..., chunks=...) or freeing GPU memory " + "_read_geotiff_dask(..., chunks=...) or freeing GPU memory " "with cupy.get_default_memory_pool().free_all_blocks()." ) diff --git a/xrspatial/geotiff/_reader.py b/xrspatial/geotiff/_reader.py index 34516eb05..e80641a8e 100644 --- a/xrspatial/geotiff/_reader.py +++ b/xrspatial/geotiff/_reader.py @@ -2,9 +2,9 @@ This module is private to :mod:`xrspatial.geotiff`. The supported public read entry points are :func:`xrspatial.geotiff.open_geotiff`, -:func:`xrspatial.geotiff.read_geotiff_gpu`, -:func:`xrspatial.geotiff.read_geotiff_dask`, and -:func:`xrspatial.geotiff.read_vrt`. Direct callers of the helpers +:func:`xrspatial.geotiff._read_geotiff_gpu`, +:func:`xrspatial.geotiff._read_geotiff_dask`, and +:func:`xrspatial.geotiff._read_vrt`. Direct callers of the helpers defined here bypass the DataArray-level work that the public wrappers perform (ambiguous-metadata fail-closed, nodata-to-NaN promotion, ``masked_nodata`` attr, ``transform`` / ``crs`` attrs population) and @@ -308,7 +308,7 @@ def _read_to_array(source, *, window=None, overview_level: int | None = None, # decode. A windowed read against a non-default orientation has # ambiguous semantics (does the window refer to file pixels or # display pixels?) so we reject that combo rather than guess. - # ``read_geotiff_dask`` chunks the file by issuing windowed reads, + # ``_read_geotiff_dask`` chunks the file by issuing windowed reads, # so this check also rejects ``chunks=`` for non-default # orientation; the error mentions both so the failure is easy to # diagnose if it surfaces under dask. @@ -327,7 +327,7 @@ def _read_to_array(source, *, window=None, overview_level: int | None = None, # mismatches caller-built coord arrays in ``open_geotiff`` and # surfaces as an opaque ``CoordinateValidationError``. Raising # here matches the dask path's pre-flight validator (see - # ``read_geotiff_dask`` in ``__init__.py``) so all backends + # ``_read_geotiff_dask`` in ``__init__.py``) so all backends # agree on the contract. Reuses the IFD already parsed above, # so callers pay no extra metadata-parse cost (file-like # sources are read once instead of twice). @@ -345,7 +345,7 @@ def _read_to_array(source, *, window=None, overview_level: int | None = None, # via numpy negative indexing and ``band>=samples_per_pixel`` # leaks a raw numpy ``IndexError`` with the internal slice # shape. Mirrors the dask path's pre-flight validator (see - # ``read_geotiff_dask`` in ``__init__.py``), the GPU path, and + # ``_read_geotiff_dask`` in ``__init__.py``), the GPU path, and # the HTTP path (``_read_cog_http`` above) so all backends agree # on the contract: 0-based non-negative index only. ifd_samples = ifd.samples_per_pixel diff --git a/xrspatial/geotiff/_runtime.py b/xrspatial/geotiff/_runtime.py index e9dfd58e1..cb55dda35 100644 --- a/xrspatial/geotiff/_runtime.py +++ b/xrspatial/geotiff/_runtime.py @@ -14,37 +14,37 @@ # Sentinels distinguishing "user passed this kwarg explicitly" from "user # passed nothing". A plain default of None does not work because None is -# itself a value a caller could supply. ``read_geotiff_gpu`` needs both +# itself a value a caller could supply. ``_read_geotiff_gpu`` needs both # sentinels so it can tell whether the deprecated ``gpu=`` and the new # ``on_gpu_failure=`` were *each* supplied, and refuse the ambiguous # both-supplied case regardless of which values were chosen. # ``open_geotiff`` also uses ``_ON_GPU_FAILURE_SENTINEL`` to distinguish # "caller never set on_gpu_failure" (default sentinel: skip forwarding so -# the read_geotiff_gpu signature default applies) from "caller set +# the _read_geotiff_gpu signature default applies) from "caller set # on_gpu_failure=" (forward verbatim). _GPU_DEPRECATED_SENTINEL = object() _ON_GPU_FAILURE_SENTINEL = object() -# ``write_vrt`` needs to distinguish "user passed crs_wkt= explicitly" +# ``build_vrt`` needs to distinguish "user passed crs_wkt= explicitly" # (deprecation path) from "user passed nothing" (no warning, pick CRS # from the first source). A plain default of None does not work because # None is itself a value a caller could supply alongside crs=. _CRS_WKT_DEPRECATED_SENTINEL = object() # ``open_geotiff`` needs to tell "caller never set missing_sources" (default -# sentinel: skip forwarding so the read_vrt default applies, and reject the +# sentinel: skip forwarding so the _read_vrt default applies, and reject the # kwarg up front for non-VRT sources) from "caller set missing_sources=" -# (forward verbatim to read_vrt). Mirrors the on_gpu_failure pattern. +# (forward verbatim to _read_vrt). Mirrors the on_gpu_failure pattern. _MISSING_SOURCES_SENTINEL = object() -# ``write_vrt`` historically named its first positional kwarg ``vrt_path`` -# while ``to_geotiff`` / ``write_geotiff_gpu`` use ``path``. The deprecation +# ``build_vrt`` historically named its first positional kwarg ``vrt_path`` +# while ``to_geotiff`` / ``_write_geotiff_gpu`` use ``path``. The deprecation # shim adds ``path`` as the new name and accepts ``vrt_path`` with a # DeprecationWarning. The sentinel pattern distinguishes "user passed # vrt_path= explicitly" from "user passed nothing", which is the same # rationale ``_CRS_WKT_DEPRECATED_SENTINEL`` documents above. _VRT_PATH_DEPRECATED_SENTINEL = object() -# ``write_vrt`` also needs to distinguish "user passed path= explicitly" +# ``build_vrt`` also needs to distinguish "user passed path= explicitly" # (including an explicit ``path=None``, which is an error) from "user # passed nothing" (fall through to the ``vrt_path`` shim). Without this -# sentinel, ``write_vrt(None, sources)`` silently fell through to the +# sentinel, ``build_vrt(None, sources)`` silently fell through to the # ``path is None`` branch and raised a "missing required argument" # TypeError for the wrong reason. _VRT_PATH_MISSING_SENTINEL = object() diff --git a/xrspatial/geotiff/_validation.py b/xrspatial/geotiff/_validation.py index e1aeea881..0acfbdedb 100644 --- a/xrspatial/geotiff/_validation.py +++ b/xrspatial/geotiff/_validation.py @@ -1,8 +1,8 @@ """Input validators shared by the geotiff entry points. Pure leaves over numpy dtypes and Python primitives. Called from -``to_geotiff``, ``read_geotiff_dask``, ``read_geotiff_gpu``, -``read_vrt``, and ``write_geotiff_gpu`` so the rejection rules +``to_geotiff``, ``_read_geotiff_dask``, ``_read_geotiff_gpu``, +``_read_vrt``, and ``_write_geotiff_gpu`` so the rejection rules (non-positive chunks, lossy float-to-int casts, ambiguous 3D dim layouts, tile-size multiples of 16, etc.) stay in lockstep across every backend. @@ -155,7 +155,7 @@ def _validate_writer_spatial_shape(shape, dims=None, (consistent with the writer's pre-moveaxis layout invariant), so pass ``dims`` for DataArray inputs to avoid mis-naming the axis. ``entry_point`` is the function name used in the error message so - direct callers of ``write`` / ``write_streaming`` / ``write_geotiff_gpu`` + direct callers of ``write`` / ``write_streaming`` / ``_write_geotiff_gpu`` see the function they actually invoked. Also rejects 3D inputs whose band/sample axis is zero. @@ -236,7 +236,7 @@ def _validate_tile_size(tile_size) -> None: """Validate ``tile_size`` for the tiled GeoTIFF writers. Shared by ``to_geotiff`` (when ``tiled=True``) and - ``write_geotiff_gpu`` (always tiled) so the accepted types, the + ``_write_geotiff_gpu`` (always tiled) so the accepted types, the non-positive rejection, and the multiple-of-16 hint stay in lockstep. The tiled writer computes the tile grid as ``math.ceil(width / tile_size)``; ``tile_size=0`` hits @@ -273,12 +273,12 @@ def _validate_tile_size(tile_size) -> None: def _validate_chunks_arg(chunks, *, allow_none=False): """Validate the ``chunks`` kwarg shared across the dask read entry points. - Centralises the rejection rule that ``read_geotiff_dask`` already - runs so ``read_geotiff_gpu`` and ``read_vrt`` can share the same + Centralises the rejection rule that ``_read_geotiff_dask`` already + runs so ``_read_geotiff_gpu`` and ``_read_vrt`` can share the same error format. With ``allow_none=True`` a ``None`` value passes through unchanged (used by entry points whose default is - ``chunks=None``, e.g. ``read_geotiff_gpu`` and ``read_vrt``). - With ``allow_none=False`` (default, matches ``read_geotiff_dask``) + ``chunks=None``, e.g. ``_read_geotiff_gpu`` and ``_read_vrt``). + With ``allow_none=False`` (default, matches ``_read_geotiff_dask``) a ``None`` is rejected with the same ``ValueError`` format as any other non-int / non-tuple value, so callers see a clear parameter-named error instead of a downstream ``TypeError`` from @@ -289,7 +289,7 @@ def _validate_chunks_arg(chunks, *, allow_none=False): check. Returns the coerced int when given an ``np.integer`` scalar so downstream ``isinstance(chunks, int)`` checks stay accurate. - Mirrors the chunks-validation in ``read_geotiff_dask``; extends it + Mirrors the chunks-validation in ``_read_geotiff_dask``; extends it to the GPU read and VRT read entry points. """ if chunks is None: @@ -328,7 +328,7 @@ def _validate_tile_size_arg(tile_size): """Validate the ``tile_size`` kwarg for the tiled writer entry points. Wrapper kept for backwards internal compatibility; delegates to - ``_validate_tile_size`` so to_geotiff/write_geotiff_gpu share one + ``_validate_tile_size`` so to_geotiff/_write_geotiff_gpu share one validation path (positive int + multiple-of-16 for tiled output). """ _validate_tile_size(tile_size) @@ -402,11 +402,11 @@ def _validate_dispatch_kwargs( """Validate dispatcher-level kwargs across the GeoTIFF read entry points. Holds the kwarg-rejection rules that ``open_geotiff`` used to run - inline so the three direct backends (``read_geotiff_dask``, - ``read_geotiff_gpu``, ``read_vrt``) get the same validation when + inline so the three direct backends (``_read_geotiff_dask``, + ``_read_geotiff_gpu``, ``_read_vrt``) get the same validation when called directly. Before this helper, a caller who passed - ``max_cloud_bytes`` straight to ``read_geotiff_dask`` (or - ``band_nodata`` to ``read_geotiff_gpu``) got no error at all because + ``max_cloud_bytes`` straight to ``_read_geotiff_dask`` (or + ``band_nodata`` to ``_read_geotiff_gpu``) got no error at all because the kwarg either silently dropped or raised an unrelated ``TypeError`` from the signature. @@ -439,12 +439,12 @@ def _validate_dispatch_kwargs( a ``.vrt`` extension via ``isinstance(source, str)``. gpu : bool True when the call routes through the GPU pipeline (either - ``open_geotiff(gpu=True)`` or a direct ``read_geotiff_gpu``). + ``open_geotiff(gpu=True)`` or a direct ``_read_geotiff_gpu``). This is the dispatch bool, type-checked via ``_validate_gpu_arg``. Not to be confused with - ``read_geotiff_gpu``'s own deprecated ``gpu='strict'/'auto'/ + ``_read_geotiff_gpu``'s own deprecated ``gpu='strict'/'auto'/ 'loose'`` string parameter (the legacy ``on_gpu_failure`` - alias), which never reaches this helper -- ``read_geotiff_gpu`` + alias), which never reaches this helper -- ``_read_geotiff_gpu`` passes a literal ``True`` here. chunks : int, tuple, or None Caller's ``chunks=`` value. ``None`` means eager. @@ -864,7 +864,7 @@ def _validate_no_rotated_affine(attrs, *, drop_rotation: bool, mapping will be lost on write; the check returns silently. entry_point : str Name of the calling writer for the error message (``to_geotiff``, - ``write_geotiff_gpu``). Lets the two writers surface the same + ``_write_geotiff_gpu``). Lets the two writers surface the same wording while still naming the opt-in correctly for either entry point. """ @@ -998,7 +998,7 @@ def validate_write_metadata(context: Mapping[str, Any] | None = None) -> None: """Run all registered write-side ambiguous-metadata checks. Mirror of ``validate_read_metadata`` for ``to_geotiff`` / - ``write_geotiff_gpu`` / ``write_vrt``. See that docstring for the + ``_write_geotiff_gpu`` / ``build_vrt``. See that docstring for the context-schema convention and the no-op-when-empty guarantee. """ if not _WRITE_METADATA_CHECKS: @@ -1532,8 +1532,8 @@ def _same_nodata(a: float, b: float) -> bool: # Callers that still want the legacy flatten-to-first-band behaviour -# pass ``band_nodata='first'`` to ``read_vrt`` / ``open_geotiff`` / -# ``read_geotiff_dask``; the explicit opt-in surfaces the per-band +# pass ``band_nodata='first'`` to ``_read_vrt`` / ``open_geotiff`` / +# ``_read_geotiff_dask``; the explicit opt-in surfaces the per-band # ambiguity at the call site instead of papering over it silently. register_read_metadata_check(_check_read_mixed_band_metadata) diff --git a/xrspatial/geotiff/_vrt.py b/xrspatial/geotiff/_vrt.py index 8d084fd3a..e3bb67bbe 100644 --- a/xrspatial/geotiff/_vrt.py +++ b/xrspatial/geotiff/_vrt.py @@ -1979,7 +1979,7 @@ def write_vrt(vrt_path: str, source_files: list[str], *, the first source's per-band nodata is used. Integer sentinels (e.g. ``65535`` for uint16, ``-9999`` for int32) are accepted so the surface lines up with the ``nodata`` kwarg on - ``to_geotiff`` and ``write_geotiff_gpu``. + ``to_geotiff`` and ``_write_geotiff_gpu``. Returns ------- diff --git a/xrspatial/geotiff/_vrt_validation.py b/xrspatial/geotiff/_vrt_validation.py index 3bb4303aa..c97c8c783 100644 --- a/xrspatial/geotiff/_vrt_validation.py +++ b/xrspatial/geotiff/_vrt_validation.py @@ -3,14 +3,14 @@ A single :func:`validate_parsed_vrt` entry point that audits an already-parsed :class:`xrspatial.geotiff._vrt.VRTDataset` against every capability the read pipeline does not honour. Both the direct -``read_vrt`` entry point in ``_backends/vrt.py`` and the dispatched +``_read_vrt`` entry point in ``_backends/vrt.py`` and the dispatched ``open_geotiff('foo.vrt')`` branch in ``__init__.py`` call this validator before any source bytes are decoded, so the two entry points produce equivalent failures for the same bad input. Why centralise: prior to this module the capability checks were spread -across ``_vrt.read_vrt`` (per-source ``SrcRect`` / ``DstRect`` rejects -mid-decode), ``_backends/vrt.read_vrt`` (CRS / rotated-transform / +across ``_vrt._read_vrt`` (per-source ``SrcRect`` / ``DstRect`` rejects +mid-decode), ``_backends/vrt._read_vrt`` (CRS / rotated-transform / mixed-nodata checks via ``validate_read_metadata`` at the eager boundary), and ``_check_resample_alg_supported`` (per-source resample reject at the placement site). Under chunked dispatch the per-source @@ -71,7 +71,7 @@ def validate_parsed_vrt( parsed : VRTDataset The already-parsed VRT structure. The validator never re-parses XML; callers feed in the output of ``parse_vrt`` or the value - threaded through ``read_vrt(..., parsed=...)``. + threaded through ``_read_vrt(..., parsed=...)``. source : str Path to the ``.vrt`` file. Used only for error messages so a caller can locate the offending file without re-parsing. diff --git a/xrspatial/geotiff/_writer.py b/xrspatial/geotiff/_writer.py index 0ccaaad39..33becca0a 100644 --- a/xrspatial/geotiff/_writer.py +++ b/xrspatial/geotiff/_writer.py @@ -2,8 +2,8 @@ This module is private to :mod:`xrspatial.geotiff`. The supported public write entry points are :func:`xrspatial.geotiff.to_geotiff`, -:func:`xrspatial.geotiff.write_geotiff_gpu`, and -:func:`xrspatial.geotiff.write_vrt`. Direct callers of the helpers +:func:`xrspatial.geotiff._write_geotiff_gpu`, and +:func:`xrspatial.geotiff.build_vrt`. Direct callers of the helpers defined here bypass the DataArray-level validation that the public wrappers run (``transform`` derivation, ``masked_nodata`` handling, ``band``-first dim reordering, ...) and must accept the resulting byte diff --git a/xrspatial/geotiff/_writers/__init__.py b/xrspatial/geotiff/_writers/__init__.py index 4cdbbfd1b..d1c6abf92 100644 --- a/xrspatial/geotiff/_writers/__init__.py +++ b/xrspatial/geotiff/_writers/__init__.py @@ -1,7 +1,7 @@ """Writer entry points for the geotiff module. -Holds ``to_geotiff`` (and its eager-path helpers), ``write_geotiff_gpu``, -and ``write_vrt`` in sibling modules. The package ``__init__`` stays +Holds ``to_geotiff`` (and its eager-path helpers), ``_write_geotiff_gpu``, +and ``build_vrt`` in sibling modules. The package ``__init__`` stays empty so nothing leaks into ``xrspatial.geotiff`` through implicit re-exports. """ diff --git a/xrspatial/geotiff/_writers/eager.py b/xrspatial/geotiff/_writers/eager.py index 1ea7fd22a..42df77999 100644 --- a/xrspatial/geotiff/_writers/eager.py +++ b/xrspatial/geotiff/_writers/eager.py @@ -4,7 +4,7 @@ ``_write_single_tile`` (per-tile worker used by ``_write_vrt_tiled``), and ``_write_vrt_tiled`` (the deprecated ``vrt_tiled=True`` path on ``to_geotiff``). Companion modules ``_writers/gpu.py`` and -``_writers/vrt.py`` hold the GPU writer and the public ``write_vrt``; +``_writers/vrt.py`` hold the GPU writer and the public ``build_vrt``; ``to_geotiff`` dispatches to them when the caller asks for a GPU output or a ``.vrt`` path. """ @@ -39,7 +39,7 @@ _validate_tile_size_arg, _validate_writer_spatial_shape, validate_write_metadata) from .._writer import _COG_REQUIRES_TILED_MSG, write -from .gpu import write_geotiff_gpu +from .gpu import _write_geotiff_gpu def to_geotiff(data: xr.DataArray | np.ndarray, @@ -226,7 +226,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, speak classic TIFF cannot open the output. Force BigTIFF (64-bit offsets). None (default) auto-promotes when the estimated file size would exceed the classic-TIFF 4 GB limit. - Matches the same kwarg on ``write_geotiff_gpu``. + Matches the same kwarg on ``_write_geotiff_gpu``. gpu : bool or None [experimental] Requires cupy + numba CUDA, plus the optional nvCOMP / nvJPEG / nvJPEG2K libraries for codec-specific @@ -295,7 +295,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, internal-only JPEG path keeps its own dedicated ``allow_internal_only_jpeg`` flag because internal-only is a stricter tier than experimental. The kwarg is forwarded - unchanged to ``write_geotiff_gpu`` on the GPU dispatch path. + unchanged to ``_write_geotiff_gpu`` on the GPU dispatch path. allow_internal_only_jpeg : bool [internal-only] Opt in to the ``compression='jpeg'`` encode path (default ``False``). The encoder writes self-contained @@ -307,7 +307,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, the flag set, the write proceeds and a ``GeoTIFFFallbackWarning`` is emitted at call time. Without the flag, ``compression='jpeg'`` raises ``ValueError``. The - kwarg is forwarded unchanged to ``write_geotiff_gpu`` on the + kwarg is forwarded unchanged to ``_write_geotiff_gpu`` on the GPU dispatch path so callers can reach the same experimental encode via ``to_geotiff(..., gpu=True)``. allow_unparseable_crs : bool @@ -342,7 +342,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, str or binary file-like The ``path`` argument (a string for filesystem paths, the file-like object for BytesIO destinations). Returning the path - lines up with ``write_vrt`` and lets callers chain a write into + lines up with ``build_vrt`` and lets callers chain a write into a read without round-tripping through a variable; existing callers that discarded the previous ``None`` return are unaffected. @@ -398,7 +398,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, # non-positive tile_size with cog=True drove the overview loop # into a hang once oh, ow halved to 0. Validate # tile_size whenever either path will consume it: tiled output OR - # COG overview generation. Shared with write_geotiff_gpu via + # COG overview generation. Shared with _write_geotiff_gpu via # _validate_tile_size_arg so both writers keep identical validation. if tiled or cog: _validate_tile_size_arg(tile_size) @@ -511,7 +511,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, # reader round-trips because Pillow re-decodes the JFIF stream # directly, masking the interop break. Refuse the write by # default and surface the same ``allow_internal_only_jpeg=True`` - # opt-in that ``write_geotiff_gpu`` already accepts, so the + # opt-in that ``_write_geotiff_gpu`` already accepts, so the # auto-dispatch entry point can reach the experimental # internal-reader-only path the explicit GPU entry point # exposes. @@ -525,7 +525,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, "opt in to the experimental internal-reader-only path " "(issue #1845).") # The JPEG opt-in warning is emitted below once we know the - # dispatch decision: ``write_geotiff_gpu`` emits its own warning + # dispatch decision: ``_write_geotiff_gpu`` emits its own warning # on the GPU path, so emitting here would double-warn callers # of ``to_geotiff(gpu=True, compression='jpeg', # allow_internal_only_jpeg=True)``. @@ -539,7 +539,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, # so callers learn the opt-in name from the rejection message # and can fix the call site in one line. The opt-in warning is # emitted below once the GPU dispatch decision is known so the - # GPU path does not double-warn (``write_geotiff_gpu`` emits its + # GPU path does not double-warn (``_write_geotiff_gpu`` emits its # own warning on the GPU path). if (compression.lower() in _EXPERIMENTAL_CODECS and not allow_experimental_codecs): @@ -581,7 +581,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, isinstance(path, str) and path.lower().endswith('.vrt')) # Resolve GPU dispatch up front so the JPEG opt-in warning fires - # exactly once. ``write_geotiff_gpu`` emits its own warning on the + # exactly once. ``_write_geotiff_gpu`` emits its own warning on the # GPU path; emitting here as well would double-warn callers of # ``to_geotiff(gpu=True, compression='jpeg', # allow_internal_only_jpeg=True)``. VRT and CPU paths receive the @@ -604,7 +604,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, ) # Tier 3 experimental-codec opt-in warning. Mirrors the JPEG # flag's "warn once, after dispatch is resolved" shape: - # ``write_geotiff_gpu`` emits its own warning on the GPU path with + # ``_write_geotiff_gpu`` emits its own warning on the GPU path with # a backend-specific caveat, so the CPU dispatcher only warns when # the write is staying on CPU. if (isinstance(compression, str) @@ -699,12 +699,12 @@ def to_geotiff(data: xr.DataArray | np.ndarray, drop_rotation=drop_rotation) return path - # Dispatch to write_geotiff_gpu when GPU was selected (explicit + # Dispatch to _write_geotiff_gpu when GPU was selected (explicit # ``gpu=True`` or auto-detected CuPy data). ``auto_detected_gpu`` # and ``use_gpu`` were computed above to gate the JPEG opt-in # warning; reuse them so the call sites stay in sync. if use_gpu and _path_is_file_like: - # write_geotiff_gpu's nvCOMP path materialises tile parts and then + # _write_geotiff_gpu's nvCOMP path materialises tile parts and then # calls _write_bytes(path), which would write at the buffer's # current cursor without truncating. More importantly, the GPU # path was never tested with file-like destinations; refuse rather @@ -727,7 +727,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, "tiled=False is not supported on the GPU writer. " "Pass gpu=False or omit tiled=False.") try: - write_geotiff_gpu( + _write_geotiff_gpu( data, path, crs=crs, nodata=nodata, compression=compression, compression_level=compression_level, @@ -747,7 +747,7 @@ def to_geotiff(data: xr.DataArray | np.ndarray, ) return path except ImportError as e: - # ``write_geotiff_gpu`` raises ImportError when cupy itself + # ``_write_geotiff_gpu`` raises ImportError when cupy itself # can't be imported. nvCOMP absence doesn't surface here: # ``_try_nvcomp_from_device_bufs`` returns None when the # library can't load, and the writer drops to CPU @@ -1246,7 +1246,7 @@ def _write_vrt_tiled(data, vrt_path, *, crs=None, nodata=None, wkt_fallback = wkt if nodata is None: # Use the same alias-aware resolver that to_geotiff / - # write_geotiff_gpu apply so a rioxarray-style DataArray + # _write_geotiff_gpu apply so a rioxarray-style DataArray # (``attrs['nodatavals']``) or a CF-style one # (``attrs['_FillValue']``) round-trips through ``.vrt`` # the same way it does through ``.tif``. Using diff --git a/xrspatial/geotiff/_writers/gpu.py b/xrspatial/geotiff/_writers/gpu.py index aca864bc3..345ba4f0b 100644 --- a/xrspatial/geotiff/_writers/gpu.py +++ b/xrspatial/geotiff/_writers/gpu.py @@ -1,6 +1,6 @@ """GPU writer entry point. -Holds ``write_geotiff_gpu``, which compresses tiles on the device via +Holds ``_write_geotiff_gpu``, which compresses tiles on the device via nvCOMP (when available) and falls back to the eager CPU writer when nvCOMP is missing or the device path raises under ``on_gpu_failure='auto'``. @@ -36,7 +36,7 @@ def _compute_gpu_samples_hint(data) -> int: """Return the band count using the same convention the GPU writer's band-first -> band-last remap uses. - The remap below in ``write_geotiff_gpu`` moves bands from + The remap below in ``_write_geotiff_gpu`` moves bands from ``shape[0]`` to ``shape[2]`` for band-first DataArrays. The MinIsWhite single-band guard runs *before* that remap, so reading ``data.shape[2]`` blindly would treat a band-first ``(1, H, W)`` @@ -58,27 +58,27 @@ def _compute_gpu_samples_hint(data) -> int: return int(data.shape[2]) -def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, - path: str | BinaryIO, *, - crs: int | str | None = None, - nodata: float | int | None = None, - compression: str = 'zstd', - compression_level: int | None = None, - tiled: bool = True, - tile_size: int = 256, - predictor: bool | int = False, - cog: bool = False, - overview_levels: list[int] | None = None, - overview_resampling: str = 'mean', - bigtiff: bool | None = None, - streaming_buffer_bytes: int = 256 * 1024 * 1024, - max_z_error: float = 0.0, - photometric: str | int = 'auto', - allow_internal_only_jpeg: bool = False, - allow_experimental_codecs: bool = False, - allow_unparseable_crs: bool = False, - drop_rotation: bool = False, - ) -> str | BinaryIO: +def _write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, + path: str | BinaryIO, *, + crs: int | str | None = None, + nodata: float | int | None = None, + compression: str = 'zstd', + compression_level: int | None = None, + tiled: bool = True, + tile_size: int = 256, + predictor: bool | int = False, + cog: bool = False, + overview_levels: list[int] | None = None, + overview_resampling: str = 'mean', + bigtiff: bool | None = None, + streaming_buffer_bytes: int = 256 * 1024 * 1024, + max_z_error: float = 0.0, + photometric: str | int = 'auto', + allow_internal_only_jpeg: bool = False, + allow_experimental_codecs: bool = False, + allow_unparseable_crs: bool = False, + drop_rotation: bool = False, + ) -> str | BinaryIO: """Write a CuPy-backed DataArray as a GeoTIFF with GPU compression. Release-contract tier (see @@ -175,7 +175,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, [experimental] Tile size in pixels (default 256). Must be a positive multiple of 16; this is a TIFF 6 spec requirement on TileWidth and TileLength for broad reader compatibility. - ``write_geotiff_gpu`` is always tiled, so the check fires for + ``_write_geotiff_gpu`` is always tiled, so the check fires for every call. predictor : bool or int [experimental] TIFF predictor. ``False``/``0``/``1`` -> none, @@ -261,7 +261,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, str or binary file-like The ``path`` argument (a string for filesystem paths, the file-like object for BytesIO destinations). Returning the path - mirrors ``to_geotiff`` and ``write_vrt`` so callers can handle + mirrors ``to_geotiff`` and ``build_vrt`` so callers can handle the three writers uniformly. Raises @@ -276,7 +276,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, """ if not tiled: raise ValueError( - "write_geotiff_gpu requires tiled=True. nvCOMP batch " + "_write_geotiff_gpu requires tiled=True. nvCOMP batch " "compression is tile-based; the strip layout is not " "implemented on the GPU path. Use to_geotiff(..., gpu=False, " "tiled=False) for strip output on CPU.") @@ -303,7 +303,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, and compression.lower() == 'jpeg' and allow_internal_only_jpeg): warnings.warn( - "write_geotiff_gpu(compression='jpeg', " + "_write_geotiff_gpu(compression='jpeg', " "allow_internal_only_jpeg=True) writes JFIF tiles without " "the TIFF JPEGTables tag (347); the file decodes through " "xrspatial but may fail in libtiff, GDAL, or rasterio. " @@ -334,7 +334,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, if (_gpu_codec in _EXPERIMENTAL_CODECS and allow_experimental_codecs): warnings.warn( - f"write_geotiff_gpu(compression={compression!r}, " + f"_write_geotiff_gpu(compression={compression!r}, " "allow_experimental_codecs=True): experimental codec, " "GPU encode path is not byte-identical to the CPU writer " "(different backend libraries). See issue #2137.", @@ -360,29 +360,29 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, "Move the array to host memory and call to_geotiff with " "gpu=False, or write with photometric='minisblack' / " "'auto'.") - # write_geotiff_gpu is always tiled, so validate tile_size here and + # _write_geotiff_gpu is always tiled, so validate tile_size here and # keep parity with the public to_geotiff entry point. _validate_tile_size_arg(tile_size) _validate_nodata_arg(nodata) # Refuse to silently drop ``attrs['rotated_affine']``. Mirror the # gate ``to_geotiff`` runs upstream so direct callers of - # ``write_geotiff_gpu`` get the same rejection. + # ``_write_geotiff_gpu`` get the same rejection. _drop_rotation_attrs = getattr(data, 'attrs', None) or {} _validate_no_rotated_affine( _drop_rotation_attrs, drop_rotation=drop_rotation, - entry_point="write_geotiff_gpu", + entry_point="_write_geotiff_gpu", ) - # Reject empty spatial shapes. ``write_geotiff_gpu`` is a public + # Reject empty spatial shapes. ``_write_geotiff_gpu`` is a public # entry point and direct callers (with cupy.ndarray or raw # numpy) do not flow through ``to_geotiff``'s guard, so check here # before any GPU work starts. _validate_writer_spatial_shape( getattr(data, 'shape', None), getattr(data, 'dims', None), - entry_point="write_geotiff_gpu", + entry_point="_write_geotiff_gpu", ) # Reject ``gdal_metadata_xml`` / ``extra_tags`` pass-through writes @@ -393,7 +393,7 @@ def write_geotiff_gpu(data: xr.DataArray | cupy.ndarray | np.ndarray, _validate_write_rich_tag_optin( _attrs_for_optin, allow_experimental_codecs=allow_experimental_codecs, - entry_point="write_geotiff_gpu", + entry_point="_write_geotiff_gpu", ) # Ambiguous-metadata checks; mirrors ``to_geotiff`` so the GPU diff --git a/xrspatial/geotiff/_writers/vrt.py b/xrspatial/geotiff/_writers/vrt.py index 870a3e260..cd8128d78 100644 --- a/xrspatial/geotiff/_writers/vrt.py +++ b/xrspatial/geotiff/_writers/vrt.py @@ -1,9 +1,9 @@ """VRT writer entry point. -Wraps ``_vrt.write_vrt`` with the public ``write_vrt`` surface: +Wraps ``_vrt.build_vrt`` with the public ``build_vrt`` surface: deprecation handling for the ``crs_wkt`` and ``vrt_path`` aliases, normalisation of the ``crs`` kwarg to WKT via ``_resolve_crs_to_wkt``, -and the parity surface vs ``to_geotiff`` / ``write_geotiff_gpu``. +and the parity surface vs ``to_geotiff`` / ``_write_geotiff_gpu``. """ from __future__ import annotations @@ -15,7 +15,7 @@ from .._validation import _validate_nodata_arg -def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, +def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, source_files: list[str] | None = None, *, vrt_path: str | None = _VRT_PATH_DEPRECATED_SENTINEL, relative: bool = True, @@ -59,7 +59,7 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, ---------- path : str [advanced] Output .vrt file path. Mirrors the ``path`` kwarg - on ``to_geotiff`` and ``write_geotiff_gpu`` so the writer trio + on ``to_geotiff`` and ``_write_geotiff_gpu`` so the writer trio shares a single destination-arg name. source_files : list of str [advanced] Paths to the source GeoTIFF files. @@ -67,8 +67,8 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, [internal-only] Deprecated alias for ``path``. Emits ``DeprecationWarning`` when supplied; passing both ``path`` and ``vrt_path`` raises ``TypeError``. Kept so existing - callers (``write_vrt(vrt_path, sources)`` positional or - ``write_vrt(vrt_path=...)`` keyword) keep working through the + callers (``build_vrt(vrt_path, sources)`` positional or + ``build_vrt(vrt_path=...)`` keyword) keep working through the deprecation window. New code should use ``path``. relative : bool, optional [advanced] Store source paths relative to the VRT file @@ -76,7 +76,7 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, crs : int, str, or None, optional [advanced] EPSG code (int), WKT string, or PROJ string. If None, the CRS is taken from the first source GeoTIFF. Mirrors - the ``crs`` kwarg on ``to_geotiff`` and ``write_geotiff_gpu`` + the ``crs`` kwarg on ``to_geotiff`` and ``_write_geotiff_gpu`` so the same value can be forwarded to whichever writer the caller picked without per-writer special-casing. crs_wkt : str or None, optional @@ -94,7 +94,7 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, GeoTIFF. Integer sentinels (e.g. ``65535`` for uint16, ``-9999`` for int32) are accepted so the surface lines up with the - ``nodata`` kwarg on ``to_geotiff`` and ``write_geotiff_gpu``. + ``nodata`` kwarg on ``to_geotiff`` and ``_write_geotiff_gpu``. Returns ------- @@ -108,8 +108,8 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, below are illustrative; replace with paths to real GeoTIFF files on disk: - >>> from xrspatial.geotiff import write_vrt, open_geotiff - >>> vrt_path = write_vrt( # doctest: +SKIP + >>> from xrspatial.geotiff import build_vrt, open_geotiff + >>> vrt_path = build_vrt( # doctest: +SKIP ... 'mosaic.vrt', ... source_files=['tile_west.tif', 'tile_east.tif'], ... ) @@ -117,14 +117,14 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, Intentionally raises (on the read side). If the source tiles disagree on their per-band nodata sentinels, the default - ``band_nodata=None`` on ``open_geotiff`` / ``read_vrt`` rejects + ``band_nodata=None`` on ``open_geotiff`` / ``_read_vrt`` rejects the mosaic with ``MixedBandMetadataError``. The writer does not pre-validate cross-tile metadata; the failure mode lives on the read side: >>> from xrspatial.geotiff import MixedBandMetadataError >>> # tile_a.tif declares nodata=-9999; tile_b.tif declares nodata=0 - >>> bad_path = write_vrt( # doctest: +SKIP + >>> bad_path = build_vrt( # doctest: +SKIP ... 'mixed_nodata.vrt', ... source_files=['tile_a.tif', 'tile_b.tif'], ... ) @@ -135,7 +135,7 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, """ # Explicit signature (previously ``**kwargs``) so ``inspect.signature``, # IDE autocomplete, and ``mypy --strict`` can see the accepted kwargs - # without parsing the docstring. Mirrors ``_vrt.write_vrt`` for the + # without parsing the docstring. Mirrors ``_vrt.build_vrt`` for the # historic ``crs_wkt`` path; the new ``crs`` path normalises through # ``_resolve_crs_to_wkt`` before forwarding because the internal # writer still only speaks WKT. @@ -143,10 +143,10 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # The ``path`` / ``vrt_path`` shim resolves the destination kwarg # before any other processing so the rest of the function works # uniformly against a single ``vrt_path`` local. ``path`` is the - # new name (parity with to_geotiff / write_geotiff_gpu); ``vrt_path`` + # new name (parity with to_geotiff / _write_geotiff_gpu); ``vrt_path`` # is kept as a deprecated alias to preserve back-compat for callers - # using either positional ``write_vrt(vrt_path, sources)`` or - # keyword ``write_vrt(vrt_path=...)``. + # using either positional ``build_vrt(vrt_path, sources)`` or + # keyword ``build_vrt(vrt_path=...)``. path_passed = path is not _VRT_PATH_MISSING_SENTINEL vrt_path_passed = vrt_path is not _VRT_PATH_DEPRECATED_SENTINEL if path_passed and vrt_path_passed: @@ -155,13 +155,13 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # picking one. Mirrors the same rule the ``crs`` / ``crs_wkt`` # shim below applies. raise TypeError( - "write_vrt: pass either 'path' or the deprecated 'vrt_path' " + "build_vrt: pass either 'path' or the deprecated 'vrt_path' " "alias, not both.") if vrt_path_passed: warnings.warn( - "write_vrt(..., vrt_path=...) is deprecated; use path=... " + "build_vrt(..., vrt_path=...) is deprecated; use path=... " "instead. The kwarg was renamed for parity with to_geotiff " - "and write_geotiff_gpu, which already accept 'path' as the " + "and _write_geotiff_gpu, which already accept 'path' as the " "destination kwarg.", DeprecationWarning, stacklevel=2, @@ -172,19 +172,19 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # required positional argument`` semantics by raising rather than # forwarding the sentinel into ``_write_vrt_internal``. raise TypeError( - "write_vrt: missing required argument 'path'") + "build_vrt: missing required argument 'path'") if path is None: - # Explicit ``path=None`` (including positional ``write_vrt(None, + # Explicit ``path=None`` (including positional ``build_vrt(None, # sources)``) is rejected up front so the error message names the # offending kwarg instead of crashing deep in # ``os.path.dirname(os.path.abspath(None))``. The sentinel default # on ``path`` is what lets us distinguish this case from "caller # passed nothing" above. raise TypeError( - "write_vrt: 'path' must be a str, got None") + "build_vrt: 'path' must be a str, got None") if source_files is None: raise TypeError( - "write_vrt: missing required argument 'source_files'") + "build_vrt: missing required argument 'source_files'") crs_wkt_passed = crs_wkt is not _CRS_WKT_DEPRECATED_SENTINEL if crs is not None and crs_wkt_passed: @@ -192,21 +192,21 @@ def write_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # to encode the same CRS as the int. Refuse rather than silently # picking one. raise TypeError( - "write_vrt: pass either 'crs' or the deprecated 'crs_wkt' " + "build_vrt: pass either 'crs' or the deprecated 'crs_wkt' " "alias, not both.") if crs_wkt_passed: warnings.warn( - "write_vrt(..., crs_wkt=...) is deprecated; use crs=... " + "build_vrt(..., crs_wkt=...) is deprecated; use crs=... " "instead. The kwarg was renamed for parity with to_geotiff " - "and write_geotiff_gpu, which already accept 'crs' as either " + "and _write_geotiff_gpu, which already accept 'crs' as either " "an int EPSG code or a WKT string.", DeprecationWarning, stacklevel=2, ) crs = crs_wkt - # Reject bool / non-numeric nodata at the entry point so write_vrt - # matches the to_geotiff / write_geotiff_gpu surface. ``bool`` is a + # Reject bool / non-numeric nodata at the entry point so build_vrt + # matches the to_geotiff / _write_geotiff_gpu surface. ``bool`` is a # subclass of ``int`` in Python, so a typo like ``nodata=True`` would # slip past every downstream ``isinstance(nodata, (int, float))`` # guard and the VRT XML emitter would write ``True diff --git a/xrspatial/geotiff/tests/attrs/test_contract.py b/xrspatial/geotiff/tests/attrs/test_contract.py index fadcb17bf..15c50f29b 100644 --- a/xrspatial/geotiff/tests/attrs/test_contract.py +++ b/xrspatial/geotiff/tests/attrs/test_contract.py @@ -32,7 +32,7 @@ from xrspatial.geotiff import ConflictingNodataError from xrspatial.geotiff import _attrs as _attrs_module -from xrspatial.geotiff import open_geotiff, read_vrt, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_vrt, to_geotiff from xrspatial.geotiff._attrs import _ATTRS_CONTRACT_VERSION, _resolve_nodata_attr from .._helpers.markers import requires_gpu @@ -931,11 +931,11 @@ def test_version_stamp_present_per_tiff_backend(tmp_path, opener, label): def _v_vrt_eager(path): - return read_vrt(path) + return _read_vrt(path) def _v_vrt_chunked(path): - return read_vrt(path, chunks=32) + return _read_vrt(path, chunks=32) _VERSION_VRT_OPENERS = [ diff --git a/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py b/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py index b650843a9..863d1cc98 100644 --- a/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py +++ b/xrspatial/geotiff/tests/golden_corpus/test_dask_numpy.py @@ -6,7 +6,7 @@ oracle pulls the candidate's pixels via ``.compute()`` under the hood (``_candidate_pixels`` is dask-aware), so the comparison machinery is the same. This exercises the windowed-decode plumbing inside -``read_geotiff_dask``: any divergence between the eager and dask reads +``_read_geotiff_dask``: any divergence between the eager and dask reads shows up here. The skip / xfail taxonomy is intentionally identical to the eager diff --git a/xrspatial/geotiff/tests/golden_corpus/test_vrt.py b/xrspatial/geotiff/tests/golden_corpus/test_vrt.py index e4fc3b25e..081df0b81 100644 --- a/xrspatial/geotiff/tests/golden_corpus/test_vrt.py +++ b/xrspatial/geotiff/tests/golden_corpus/test_vrt.py @@ -2,20 +2,20 @@ The VRT path in ``xrspatial.geotiff`` is reached when ``open_geotiff`` sees a ``.vrt`` source path; it -delegates to ``read_vrt`` which parses the GDAL VRT XML and stitches +delegates to ``_read_vrt`` which parses the GDAL VRT XML and stitches the listed source GeoTIFFs back into a single DataArray. This module synthesises a two-source horizontal mosaic from one corpus fixture: the source ``.tif`` is rasterio-copied twice into a temp directory with the second copy's origin shifted east by one image -width, then ``write_vrt`` builds the VRT XML, and ``open_geotiff`` +width, then ``build_vrt`` builds the VRT XML, and ``open_geotiff`` reads it back. The oracle reads the same VRT through rasterio so any divergence is in xrspatial's VRT plumbing, not in the mosaic geometry. A separate cell uses the COG fixture as the source. Its transform is the manifest default (``[0.001, 0, -120, 0, -0.001, 45]``); the right-half copy is shifted by ``+pixel_width * width`` so the two -copies do not overlap, which is what ``write_vrt`` expects for a clean +copies do not overlap, which is what ``build_vrt`` expects for a clean mosaic. The expected mosaic has shape ``(H, 2 * W)``. VRT-specific gaps -- if any surface -- go in ``_VRT_SKIPS``. The @@ -33,7 +33,7 @@ pytest.importorskip("yaml") rasterio = pytest.importorskip("rasterio") -from xrspatial.geotiff import open_geotiff, write_vrt # noqa: E402 +from xrspatial.geotiff import open_geotiff, build_vrt # noqa: E402 # Golden-corpus fixtures span every codec/tier, including the # experimental and internal-only ones. Opting in here lets the parity @@ -88,7 +88,7 @@ def _build_two_source_vrt( dst.write(data, 1) vrt_path = tmp_dir / "mosaic.vrt" - write_vrt(str(vrt_path), [str(left), str(right)]) + build_vrt(str(vrt_path), [str(left), str(right)]) expected = np.concatenate([data, data], axis=1) return vrt_path, expected diff --git a/xrspatial/geotiff/tests/gpu/test_codec.py b/xrspatial/geotiff/tests/gpu/test_codec.py index ad41110b4..3f28223e1 100644 --- a/xrspatial/geotiff/tests/gpu/test_codec.py +++ b/xrspatial/geotiff/tests/gpu/test_codec.py @@ -110,7 +110,7 @@ def test_gpu_write_roundtrip_after_batched_compress_1712(compression): """GPU compress path round-trips uncorrupted for deflate + zstd.""" import cupy - from xrspatial.geotiff import open_geotiff, write_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _write_geotiff_gpu rng = np.random.default_rng(seed=1712) arr_cpu = rng.random((512, 512), dtype=np.float32) @@ -120,7 +120,7 @@ def test_gpu_write_roundtrip_after_batched_compress_1712(compression): with tempfile.TemporaryDirectory(prefix="nvcomp_batch_1712_") as td: path = os.path.join(td, f"roundtrip_{compression}.tif") try: - write_geotiff_gpu( + _write_geotiff_gpu( darr, path, compression=compression, tiled=True, @@ -138,15 +138,15 @@ def test_gpu_write_zero_tile_edge_case_1712(): """A 0-tile compress returns an empty list without indexing into None.""" import cupy - from xrspatial.geotiff import open_geotiff, write_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _write_geotiff_gpu arr_gpu = cupy.zeros((32, 32), dtype=cupy.float32) darr = xr.DataArray(arr_gpu, dims=["y", "x"]) with tempfile.TemporaryDirectory(prefix="nvcomp_batch_1712_") as td: path = os.path.join(td, "tiny.tif") try: - write_geotiff_gpu(darr, path, compression="zstd", - tiled=True, tile_size=32) + _write_geotiff_gpu(darr, path, compression="zstd", + tiled=True, tile_size=32) except RuntimeError as e: pytest.skip(f"nvCOMP unavailable: {e}") back = open_geotiff(path) @@ -229,7 +229,7 @@ def _recording(compressed_tiles, tile_bytes, compression): ]) def test_nvcomp_batch_upload_correctness_p3(tmp_path, monkeypatch, size, tile): """GPU decode of Deflate-tiled TIFFs is bit-exact vs CPU.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260508) @@ -243,7 +243,7 @@ def test_nvcomp_batch_upload_correctness_p3(tmp_path, monkeypatch, size, tile): np.testing.assert_array_equal(cpu, arr) records = _wrap_nvcomp_with_call_recorder_p3(monkeypatch) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) np.testing.assert_array_equal(gpu_da.data.get(), cpu) assert any(success for _, success in records), ( @@ -278,7 +278,7 @@ def test_nvcomp_kvikio_fallback_skips_zstd_p3(monkeypatch): @_nvcomp_only_p3 def test_nvcomp_batch_upload_perf_regression_guard_p3(tmp_path, monkeypatch): """Sanity guard: 2048x2048 Deflate-tiled GPU decode finishes quickly.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu rng = np.random.RandomState(20260508) arr = rng.randint(0, 4096, size=(2048, 2048), dtype=np.uint16) @@ -286,11 +286,11 @@ def test_nvcomp_batch_upload_perf_regression_guard_p3(tmp_path, monkeypatch): _write_deflate_tiled_p3(path, arr, tile=(128, 128)) # Warm up. - _ = read_geotiff_gpu(str(path)) + _ = _read_geotiff_gpu(str(path)) records = _wrap_nvcomp_with_call_recorder_p3(monkeypatch) t0 = time.perf_counter() - out = read_geotiff_gpu(str(path)) + out = _read_geotiff_gpu(str(path)) elapsed = time.perf_counter() - t0 assert any(success for _, success in records), ( @@ -299,7 +299,7 @@ def test_nvcomp_batch_upload_perf_regression_guard_p3(tmp_path, monkeypatch): ) assert elapsed < 0.2, ( - f"read_geotiff_gpu on 2048x2048 deflate-tiled TIFF took " + f"_read_geotiff_gpu on 2048x2048 deflate-tiled TIFF took " f"{elapsed * 1000:.1f} ms (threshold 200 ms) -- possible " f"regression in the nvCOMP batched H2D upload path" ) @@ -1014,7 +1014,7 @@ def test_rgb_jpeg_gpu_no_crash_1549(tmp_path, monkeypatch): """3-band JPEG must not raise CUDARuntimeError on GPU read.""" import cupy - from xrspatial.geotiff import _gpu_decode, read_geotiff_gpu + from xrspatial.geotiff import _gpu_decode, _read_geotiff_gpu spy = {"calls": 0, "successes": 0} original = _gpu_decode._try_nvjpeg_batch_decode @@ -1031,7 +1031,7 @@ def wrapped(*args, **kwargs): path = str(tmp_path / "rgb_jpeg_1549.tif") _write_jpeg_rgb_tiff_1549(path) - arr = read_geotiff_gpu(path, gpu='strict', allow_internal_only_jpeg=True) + arr = _read_geotiff_gpu(path, gpu='strict', allow_internal_only_jpeg=True) assert isinstance(arr.data, cupy.ndarray) decoded = arr.data.get() assert decoded.shape == (256, 256, 3) @@ -1168,11 +1168,11 @@ def _patched(data, width, height, samples=1, def _read_cpu_gpu_lerc(path): """Read *path* with both readers and return ``(cpu_array, gpu_host_array)``.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array cpu, _geo = read_to_array(path, allow_experimental_codecs=True) - gpu_da = read_geotiff_gpu( + gpu_da = _read_geotiff_gpu( path, gpu='strict', allow_experimental_codecs=True, ) gpu_host = gpu_da.data.get() @@ -1299,12 +1299,12 @@ def test_no_mask_roundtrip_bitexact(self, tmp_path): def _block_cpu_fallback_1517(monkeypatch): - """Make any call to ``read_to_array`` from ``read_geotiff_gpu`` fail loudly.""" + """Make any call to ``read_to_array`` from ``_read_geotiff_gpu`` fail loudly.""" from xrspatial.geotiff._backends import gpu as gpu_backend def _no_fallback(*args, **kwargs): raise AssertionError( - "read_geotiff_gpu fell back to read_to_array; " + "_read_geotiff_gpu fell back to read_to_array; " "the GPU decode path was not exercised." ) @@ -1319,7 +1319,7 @@ def test_gpu_predictor2_big_endian_int32_tiled_reproducer_1517(tmp_path, monkeyp import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260507) @@ -1337,7 +1337,7 @@ def test_gpu_predictor2_big_endian_int32_tiled_reproducer_1517(tmp_path, monkeyp np.testing.assert_array_equal(cpu, arr) _block_cpu_fallback_1517(monkeypatch) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.dtype(np.int32) assert gpu_da.data.dtype.isnative @@ -1354,7 +1354,7 @@ def test_gpu_predictor2_big_endian_dtypes_tiled_1517(tmp_path, monkeypatch, dtyp import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260508) @@ -1376,7 +1376,7 @@ def test_gpu_predictor2_big_endian_dtypes_tiled_1517(tmp_path, monkeypatch, dtyp np.testing.assert_array_equal(cpu, arr) _block_cpu_fallback_1517(monkeypatch) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.dtype(dtype) assert gpu_da.data.dtype.isnative @@ -1389,7 +1389,7 @@ def test_gpu_predictor2_big_endian_stripped_uint16_1517(tmp_path): import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260509) @@ -1403,7 +1403,7 @@ def test_gpu_predictor2_big_endian_stripped_uint16_1517(tmp_path): cpu, _ = read_to_array(str(path)) np.testing.assert_array_equal(cpu, arr) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.dtype(np.uint16) assert gpu_da.data.dtype.isnative @@ -1416,7 +1416,7 @@ def test_gpu_predictor2_little_endian_still_works_1517(tmp_path, monkeypatch): import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260510) @@ -1434,7 +1434,7 @@ def test_gpu_predictor2_little_endian_still_works_1517(tmp_path, monkeypatch): np.testing.assert_array_equal(cpu, arr) _block_cpu_fallback_1517(monkeypatch) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.dtype(np.int32) np.testing.assert_array_equal(gpu_da.data.get(), cpu) @@ -1446,7 +1446,7 @@ def test_gpu_predictor3_big_endian_still_works_1517(tmp_path, monkeypatch): import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260511) @@ -1462,7 +1462,7 @@ def test_gpu_predictor3_big_endian_still_works_1517(tmp_path, monkeypatch): np.testing.assert_array_equal(cpu, arr) _block_cpu_fallback_1517(monkeypatch) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.dtype(np.float32) np.testing.assert_array_equal(gpu_da.data.get(), cpu) @@ -1676,27 +1676,27 @@ def _build_predictor3_uint32_tiled_tiff_1933( @requires_gpu class TestGPUEagerRejectsMalformedFile_1933: - """``read_geotiff_gpu`` rejects predictor=3 + integer SampleFormat.""" + """``_read_geotiff_gpu`` rejects predictor=3 + integer SampleFormat.""" def test_gpu_eager_stripped_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.array( [[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.uint32) path = tmp_path / "pred3_uint32_stripped.tif" path.write_bytes(_build_predictor3_uint32_stripped_tiff_1933(arr)) with pytest.raises(ValueError, match="Predictor=3"): - read_geotiff_gpu(str(path)) + _read_geotiff_gpu(str(path)) def test_gpu_eager_tiled_raises(self, tmp_path): """Tiled layout hits the tiled GPU validator at gpu.py:443.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint32).reshape(16, 16) path = tmp_path / "pred3_uint32_tiled.tif" path.write_bytes(_build_predictor3_uint32_tiled_tiff_1933(arr)) with pytest.raises(ValueError, match="Predictor=3"): - read_geotiff_gpu(str(path)) + _read_geotiff_gpu(str(path)) def test_gpu_dispatcher_eager_raises(self, tmp_path): """``open_geotiff(gpu=True)`` dispatcher rejects the file.""" @@ -1714,25 +1714,25 @@ class TestGPUChunkedRejectsMalformedFile_1933: """The dask+GPU paths also reject predictor=3 + integer.""" def test_read_geotiff_gpu_chunked_stripped_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(64, dtype=np.uint32).reshape(8, 8) path = tmp_path / "pred3_uint32_chunked_str.tif" path.write_bytes(_build_predictor3_uint32_stripped_tiff_1933(arr)) with pytest.raises(ValueError, match="Predictor=3"): - read_geotiff_gpu(str(path), chunks=4) + _read_geotiff_gpu(str(path), chunks=4) def test_read_geotiff_gpu_chunked_tiled_raises(self, tmp_path): """Tiled chunked path with KvikIO available exercises gpu.py:999.""" pytest.importorskip("kvikio") - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint32).reshape(16, 16) path = tmp_path / "pred3_uint32_chunked_tiled.tif" path.write_bytes(_build_predictor3_uint32_tiled_tiff_1933(arr)) with pytest.raises(ValueError, match="Predictor=3"): - read_geotiff_gpu(str(path), chunks=16) + _read_geotiff_gpu(str(path), chunks=16) def test_open_geotiff_chunks_gpu_dispatcher_raises(self, tmp_path): """``open_geotiff(chunks=, gpu=True)`` dispatcher rejects the file.""" @@ -1750,7 +1750,7 @@ class TestValidPredictor3StillWorksOnGPU_1933: """A legitimate predictor=3 + float32 tiled file still decodes on GPU.""" def test_predictor3_float32_gpu_round_trip(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff arr = np.linspace(-1.0, 1.0, 256, dtype=np.float32).reshape(16, 16) path = tmp_path / "pred3_float32_tiled.tif" @@ -1759,12 +1759,12 @@ def test_predictor3_float32_gpu_round_trip(self, tmp_path): tiled=True, tile_size=16, ) - result = read_geotiff_gpu(str(path)) + result = _read_geotiff_gpu(str(path)) assert result.dtype == np.float32 np.testing.assert_array_equal(result.data.get(), arr) def test_predictor3_float32_dask_gpu_round_trip(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff arr = np.linspace(-1.0, 1.0, 256, dtype=np.float32).reshape(16, 16) path = tmp_path / "pred3_float32_dask.tif" @@ -1773,7 +1773,7 @@ def test_predictor3_float32_dask_gpu_round_trip(self, tmp_path): tiled=True, tile_size=16, ) - result = read_geotiff_gpu(str(path), chunks=8) + result = _read_geotiff_gpu(str(path), chunks=8) assert result.dtype == np.float32 np.testing.assert_array_equal(result.compute().data.get(), arr) @@ -1783,7 +1783,7 @@ class TestErrorMessageStable_1933: """The GPU error wording matches the eager/dask wording.""" def test_gpu_error_message_matches_eager(self, tmp_path): - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu arr = np.arange(64, dtype=np.uint32).reshape(8, 8) path = tmp_path / "pred3_uint32_msg.tif" @@ -1792,7 +1792,7 @@ def test_gpu_error_message_matches_eager(self, tmp_path): with pytest.raises(ValueError) as exc_eager: open_geotiff(str(path)) with pytest.raises(ValueError) as exc_gpu: - read_geotiff_gpu(str(path)) + _read_geotiff_gpu(str(path)) assert str(exc_eager.value) == str(exc_gpu.value), ( "GPU and eager paths must surface the same Predictor=3 " @@ -1805,11 +1805,11 @@ def test_gpu_error_message_matches_eager(self, tmp_path): # ============================================================ # Source: test_gpu_jpeg_interop_reject_issue_D_1845.py # -# ``write_geotiff_gpu`` mirrors ``to_geotiff`` and rejects +# ``_write_geotiff_gpu`` mirrors ``to_geotiff`` and rejects # ``compression='jpeg'`` by default. ``allow_internal_only_jpeg=True`` # opts in and emits ``GeoTIFFFallbackWarning``. -from xrspatial.geotiff import GeoTIFFFallbackWarning, write_geotiff_gpu # noqa: E402 +from xrspatial.geotiff import GeoTIFFFallbackWarning, _write_geotiff_gpu # noqa: E402 def _make_rgb_uint8_da_1845() -> xr.DataArray: @@ -1833,7 +1833,7 @@ def test_write_geotiff_gpu_rejects_jpeg_without_opt_in_1845(tmp_path): path = str(tmp_path / "rejected_issue_D_1845.tif") with pytest.raises(ValueError, match="JPEGTables"): - write_geotiff_gpu(da, path, compression='jpeg') + _write_geotiff_gpu(da, path, compression='jpeg') def test_write_geotiff_gpu_rejects_jpeg_message_mentions_alternatives_1845(tmp_path): @@ -1842,7 +1842,7 @@ def test_write_geotiff_gpu_rejects_jpeg_message_mentions_alternatives_1845(tmp_p path = str(tmp_path / "rejected_msg_issue_D_1845.tif") with pytest.raises(ValueError) as exc: - write_geotiff_gpu(da, path, compression='jpeg') + _write_geotiff_gpu(da, path, compression='jpeg') msg = str(exc.value) assert "deflate" in msg @@ -1855,7 +1855,7 @@ def test_write_geotiff_gpu_rejects_jpeg_case_insensitive_1845(tmp_path): path = str(tmp_path / "rejected_upper_issue_D_1845.tif") with pytest.raises(ValueError, match="JPEGTables"): - write_geotiff_gpu(da, path, compression='JPEG') + _write_geotiff_gpu(da, path, compression='JPEG') @requires_gpu @@ -1865,7 +1865,7 @@ def test_write_geotiff_gpu_jpeg_opt_in_emits_warning_1845(tmp_path): path = str(tmp_path / "opt_in_issue_D_1845.tif") with pytest.warns(GeoTIFFFallbackWarning, match="JPEGTables"): - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='jpeg', allow_internal_only_jpeg=True, @@ -1883,7 +1883,7 @@ def test_write_geotiff_gpu_non_jpeg_unaffected_by_flag_1845(tmp_path): with _warnings.catch_warnings(): _warnings.simplefilter("error", GeoTIFFFallbackWarning) - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='zstd', allow_internal_only_jpeg=True, diff --git a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py index ba2e51625..e1c83e8a3 100644 --- a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +++ b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py @@ -14,13 +14,13 @@ Sections (in source-file order): * ``test_gpu_cuda_preflight_1903.py`` -- CUDA preflight in - ``read_geotiff_gpu``. + ``_read_geotiff_gpu``. * ``test_gpu_kwarg_rename_1560.py`` -- ``gpu=`` -> ``on_gpu_failure=`` rename with deprecation shim. * ``test_gpu_strict_fallback_1516.py`` -- ``gpu='auto'`` warns + fallbacks; ``gpu='strict'`` re-raises. * ``test_gpu_fallback_forwards_kwargs_2238.py`` -- CPU-fallback sites - in ``read_geotiff_gpu`` forward caller kwargs. + in ``_read_geotiff_gpu`` forward caller kwargs. * ``test_kvikio_batched_pread_1688.py`` -- single-allocation batched pread in ``_try_kvikio_read_tiles``. * ``test_gds_chunked_gpu_parity_1896.py`` -- bool-band rejection and @@ -58,7 +58,7 @@ # ===================================================================== -# Section 1903 -- CUDA preflight in ``read_geotiff_gpu``. +# Section 1903 -- CUDA preflight in ``_read_geotiff_gpu``. # ===================================================================== # # When CuPy imports but the CUDA driver is @@ -131,14 +131,14 @@ def test_preflight_returns_silently_when_device_present_1903(monkeypatch): def test_read_geotiff_gpu_preflight_surface_1903(monkeypatch, tmp_path): - """End-to-end: read_geotiff_gpu raises before touching any IFDs. + """End-to-end: _read_geotiff_gpu raises before touching any IFDs. Build a real TIFF so the function gets past the file-source setup, then verify the CUDA preflight RuntimeError surfaces from the public entry point rather than from a deep cupy.asarray() call. """ from xrspatial.geotiff import to_geotiff - from xrspatial.geotiff._backends.gpu import read_geotiff_gpu + from xrspatial.geotiff._backends.gpu import _read_geotiff_gpu da = xr.DataArray( np.arange(16, dtype=np.float32).reshape(4, 4), @@ -161,7 +161,7 @@ def _raise(*_a, **_kw): _install_cupy_stub_1903(monkeypatch, get_device_count=_raise) with pytest.raises(RuntimeError, match="CUDA runtime is not usable"): - read_geotiff_gpu(path) + _read_geotiff_gpu(path) @pytest.mark.skipif( @@ -170,7 +170,7 @@ def _raise(*_a, **_kw): ) def test_preflight_when_real_cupy_present_1903(monkeypatch): """When cupy is really installed, monkeypatching the runtime symbol - works the same way -- the import in read_geotiff_gpu finds the + works the same way -- the import in _read_geotiff_gpu finds the patched attribute.""" import cupy @@ -191,37 +191,37 @@ def _raise(*_a, **_kw): # Section 1560 -- ``gpu=`` -> ``on_gpu_failure=`` rename # ===================================================================== # -# ``read_geotiff_gpu`` previously took a ``gpu={'auto','strict'}`` kwarg +# ``_read_geotiff_gpu`` previously took a ``gpu={'auto','strict'}`` kwarg # that controlled GPU-failure policy, sharing a name with the boolean -# ``gpu=`` kwarg on ``open_geotiff`` / ``to_geotiff`` / ``read_vrt``. -# Calling ``read_geotiff_gpu(path, gpu=True)`` -- the mental model after +# ``gpu=`` kwarg on ``open_geotiff`` / ``to_geotiff`` / ``_read_vrt``. +# Calling ``_read_geotiff_gpu(path, gpu=True)`` -- the mental model after # using ``open_geotiff(path, gpu=True)`` -- raised the unhelpful # ``ValueError: gpu must be 'auto' or 'strict', got True``. # # The fix renames the kwarg to ``on_gpu_failure`` and keeps ``gpu=`` as # a deprecation shim. These tests exercise the validation path only, -# which fires before the ``cupy`` import inside ``read_geotiff_gpu``; +# which fires before the ``cupy`` import inside ``_read_geotiff_gpu``; # no GPU runtime needed. def test_on_gpu_failure_invalid_value_raises_value_error_1560(): """Bad ``on_gpu_failure`` value still raises ``ValueError``.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(ValueError, match="on_gpu_failure must be"): - read_geotiff_gpu("/nonexistent.tif", on_gpu_failure='loose') + _read_geotiff_gpu("/nonexistent.tif", on_gpu_failure='loose') def test_gpu_alias_emits_deprecation_warning_1560(): """Old ``gpu=`` kwarg still routes through, with a DeprecationWarning.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always") # Pass an invalid sentinel so we don't have to mock the full GPU # pipeline; ValueError fires after the deprecation handler runs. with pytest.raises(ValueError, match="on_gpu_failure must be"): - read_geotiff_gpu("/nonexistent.tif", gpu='loose') + _read_geotiff_gpu("/nonexistent.tif", gpu='loose') deprecations = [ r for r in records if issubclass(r.category, DeprecationWarning) @@ -241,7 +241,7 @@ def test_gpu_alias_accepts_old_values_without_validation_error_1560( on broken-CUDA hosts and the test fails for an environmental reason that has nothing to do with the alias logic under test. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu # Install a stub cupy whose preflight passes; the call should then # reach the file-read stage and raise ``FileNotFoundError``. This @@ -255,7 +255,7 @@ def test_gpu_alias_accepts_old_values_without_validation_error_1560( with pytest.raises( (FileNotFoundError, OSError, ValueError) ) as exc_info: - read_geotiff_gpu("/nonexistent.tif", gpu='strict') + _read_geotiff_gpu("/nonexistent.tif", gpu='strict') # The validation ValueError carries our exact message; a generic # file-read failure is fine because it means validation passed. @@ -265,10 +265,10 @@ def test_gpu_alias_accepts_old_values_without_validation_error_1560( def test_passing_both_raises_type_error_1560(): """Mixing the new and deprecated names is ambiguous; refuse.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"): - read_geotiff_gpu( + _read_geotiff_gpu( "/nonexistent.tif", on_gpu_failure='strict', gpu='auto', @@ -288,10 +288,10 @@ def test_passing_both_raises_regardless_of_values_1560( case where the caller passes the default value explicitly alongside the deprecated alias. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(TypeError, match="pass either 'on_gpu_failure' or"): - read_geotiff_gpu( + _read_geotiff_gpu( "/nonexistent.tif", on_gpu_failure=on_gpu_failure_val, gpu=gpu_val, @@ -304,19 +304,19 @@ def test_gpu_alias_bool_no_longer_misleading_value_error_1560(): 'strict', got True``. The new error explicitly names ``on_gpu_failure`` so the rename is discoverable from the traceback. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) with pytest.raises(ValueError, match="on_gpu_failure must be"): - read_geotiff_gpu("/nonexistent.tif", gpu=True) + _read_geotiff_gpu("/nonexistent.tif", gpu=True) # ===================================================================== # Section 1516 -- ``gpu='auto'`` warns + falls back; ``'strict'`` raises # ===================================================================== # -# ``read_geotiff_gpu`` previously wrapped the GPU decode in a too-broad +# ``_read_geotiff_gpu`` previously wrapped the GPU decode in a too-broad # ``try/except Exception: pass`` that silently swallowed any failure # and fell through to the CPU path. Real GPU regressions (an # ``AttributeError``, for one) lived undetected because the user-visible result @@ -347,7 +347,7 @@ def _cuda_actually_available_1516() -> bool: cupy may be importable on a machine without a working CUDA runtime (no driver, no device, ROCm-only, etc.). The CPU-fallback branch in - ``read_geotiff_gpu`` calls ``cupy.asarray`` which would then fail at + ``_read_geotiff_gpu`` calls ``cupy.asarray`` which would then fail at allocation time. Treat that case the same as cupy-not-installed. """ try: @@ -385,7 +385,7 @@ def _ensure_cupy_stub_1516() -> bool: cuda_mod = types.ModuleType('cupy.cuda') cuda_mod.is_available = lambda: False - # Pre-flight check in ``read_geotiff_gpu`` calls + # Pre-flight check in ``_read_geotiff_gpu`` calls # ``cupy.cuda.runtime.getDeviceCount()`` to surface a clean # ``RuntimeError`` for broken-driver setups. Tests in this section # want to exercise the downstream simulated-failure paths, so the @@ -469,7 +469,7 @@ def test_default_mode_warns_on_gpu_failure_1516(tiled_tiff_path_1516, monkeypatc """Default ``gpu='auto'`` warns and falls back to the CPU result.""" inserted_stub = _ensure_cupy_stub_1516() try: - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, expected = tiled_tiff_path_1516 @@ -477,7 +477,7 @@ def test_default_mode_warns_on_gpu_failure_1516(tiled_tiff_path_1516, monkeypatc _patch_gpu_decode_to_raise_1516(monkeypatch, synthetic) with pytest.warns(RuntimeWarning, match="GPU decode failed"): - result = read_geotiff_gpu(path) + result = _read_geotiff_gpu(path) # Fallback returned the CPU-decoded data. Real cupy arrays # expose ``.get()`` to copy back to host; the numpy stub returns @@ -495,7 +495,7 @@ def test_strict_mode_reraises_1516(tiled_tiff_path_1516, monkeypatch): """``gpu='strict'`` re-raises the original GPU exception.""" inserted_stub = _ensure_cupy_stub_1516() try: - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = tiled_tiff_path_1516 @@ -503,7 +503,7 @@ def test_strict_mode_reraises_1516(tiled_tiff_path_1516, monkeypatch): _patch_gpu_decode_to_raise_1516(monkeypatch, synthetic) with pytest.raises(RuntimeError, match="simulated GPU failure"): - read_geotiff_gpu(path, gpu='strict') + _read_geotiff_gpu(path, gpu='strict') finally: if inserted_stub: _restore_cupy_1516() @@ -520,7 +520,7 @@ def test_strict_mode_reraises_second_stage_1516( """ inserted_stub = _ensure_cupy_stub_1516() try: - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = tiled_tiff_path_1516 @@ -529,7 +529,7 @@ def test_strict_mode_reraises_second_stage_1516( with pytest.raises(RuntimeError, match="simulated second-stage GPU failure"): - read_geotiff_gpu(path, gpu='strict') + _read_geotiff_gpu(path, gpu='strict') finally: if inserted_stub: _restore_cupy_1516() @@ -546,7 +546,7 @@ def test_default_mode_warns_on_second_stage_failure_1516( """ inserted_stub = _ensure_cupy_stub_1516() try: - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, expected = tiled_tiff_path_1516 @@ -555,7 +555,7 @@ def test_default_mode_warns_on_second_stage_failure_1516( with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always") - result = read_geotiff_gpu(path) + result = _read_geotiff_gpu(path) gpu_warnings = [ w for w in records @@ -580,7 +580,7 @@ def test_invalid_gpu_kwarg_rejected_1516(tiled_tiff_path_1516): """An unknown ``gpu=`` value raises ``ValueError`` with a clear message.""" inserted_stub = _ensure_cupy_stub_1516() try: - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = tiled_tiff_path_1516 @@ -591,7 +591,7 @@ def test_invalid_gpu_kwarg_rejected_1516(tiled_tiff_path_1516): match="on_gpu_failure must be 'auto' or 'strict'"): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - read_geotiff_gpu(path, gpu='loose') + _read_geotiff_gpu(path, gpu='loose') finally: if inserted_stub: _restore_cupy_1516() @@ -601,7 +601,7 @@ def test_invalid_gpu_kwarg_rejected_1516(tiled_tiff_path_1516): # Section 2238 -- CPU-fallback sites forward caller kwargs # ===================================================================== # -# ``read_geotiff_gpu`` has four CPU-fallback call sites to +# ``_read_geotiff_gpu`` has four CPU-fallback call sites to # ``_read_to_array``: # - the stripped-layout branch # - the planar=2 per-band stage-2 fallback @@ -743,7 +743,7 @@ def test_stripped_fallback_forwards_allow_rotated_2238(tmp_path, monkeypatch): behaviour; this one pins the kwarg-forwarding contract via a recorder so a later refactor cannot silently drop the kwarg again. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gpu_backend from xrspatial.geotiff._geotags import TAG_MODEL_TRANSFORMATION @@ -780,7 +780,7 @@ def test_stripped_fallback_forwards_allow_rotated_2238(tmp_path, monkeypatch): wrapper, seen = _make_kwarg_recorder_2238() monkeypatch.setattr(gpu_backend, '_read_to_array', wrapper, raising=True) - da = read_geotiff_gpu(str(src), allow_rotated=True) + da = _read_geotiff_gpu(str(src), allow_rotated=True) assert len(seen) == 1, f"expected one fallback call, got {len(seen)}" assert seen[0].get('allow_rotated') is True, ( @@ -792,7 +792,7 @@ def test_stripped_fallback_forwards_allow_rotated_2238(tmp_path, monkeypatch): @_gpu_only def test_sparse_tile_fallback_forwards_all_kwargs_2238(tmp_path, monkeypatch): """Sparse-tile fallback hands every caller kwarg to ``_read_to_array``.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gpu_backend src = tmp_path / "2238_sparse_rotated.tif" @@ -807,7 +807,7 @@ def test_sparse_tile_fallback_forwards_all_kwargs_2238(tmp_path, monkeypatch): requested_window = (0, 0, 16, 16) requested_max_pixels = 10_000 - da = read_geotiff_gpu( + da = _read_geotiff_gpu( str(src), allow_rotated=True, window=requested_window, @@ -841,7 +841,7 @@ def test_sparse_tile_fallback_forwards_all_kwargs_2238(tmp_path, monkeypatch): def test_gpu_decode_failure_fallback_forwards_all_kwargs_2238( tmp_path, monkeypatch): """``gpu_decode_tiles`` failure routes through fallback with kwargs.""" - from xrspatial.geotiff import _gpu_decode, read_geotiff_gpu + from xrspatial.geotiff import _gpu_decode, _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gpu_backend src = tmp_path / "2238_decode_fail.tif" @@ -867,7 +867,7 @@ def _raise_decode(*args, **kwargs): requested_window = (0, 0, 16, 16) requested_max_pixels = 5_000 - da = read_geotiff_gpu( + da = _read_geotiff_gpu( str(src), allow_rotated=True, window=requested_window, @@ -907,7 +907,7 @@ def test_planar2_fallback_forwards_all_kwargs_2238(tmp_path, monkeypatch): """ tifffile = pytest.importorskip("tifffile") - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gpu_backend src = tmp_path / "2238_planar2.tif" @@ -933,7 +933,7 @@ def _none_band(*args, **kwargs): requested_max_pixels = 50_000 - da = read_geotiff_gpu( + da = _read_geotiff_gpu( str(src), max_pixels=requested_max_pixels, ) @@ -971,7 +971,7 @@ def test_decode_failure_fallback_applies_window_band_2238( """ tifffile = pytest.importorskip("tifffile") - from xrspatial.geotiff import _gpu_decode, read_geotiff_gpu + from xrspatial.geotiff import _gpu_decode, _read_geotiff_gpu src = tmp_path / "2238_windowed_fallback.tif" bands, h, w = 3, 64, 64 @@ -999,7 +999,7 @@ def _raise_from_file(*args, **kwargs): requested_window = (8, 4, 40, 36) requested_band = 1 - da = read_geotiff_gpu( + da = _read_geotiff_gpu( str(src), window=requested_window, band=requested_band, @@ -1576,7 +1576,7 @@ def test_gds_chunked_lerc_mask_matches_eager_1896( The eager GPU path resolves and forwards ``masked_fill``; this test pins the chunked path to the same behaviour. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends.gpu import _read_geotiff_gpu_chunked_gds from xrspatial.geotiff._writer import write @@ -1594,7 +1594,7 @@ def invalid_pred(a): write(arr, path, compression="lerc", tiled=True, tile_size=8, nodata=float("nan")) - eager = read_geotiff_gpu( + eager = _read_geotiff_gpu( path, on_gpu_failure='strict', allow_experimental_codecs=True, ).data.get() @@ -1625,7 +1625,7 @@ def invalid_pred(a): def test_gds_chunked_lerc_mask_sentinel_nodata_1896( tmp_path, lerc_writer_with_mask_1896): """Sentinel nodata (-9999) on float LERC: chunked path matches eager.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends.gpu import _read_geotiff_gpu_chunked_gds from xrspatial.geotiff._writer import write @@ -1643,7 +1643,7 @@ def invalid_pred(a): write(arr, path, compression="lerc", tiled=True, tile_size=8, nodata=-9999.0) - eager = read_geotiff_gpu( + eager = _read_geotiff_gpu( path, on_gpu_failure='strict', allow_experimental_codecs=True, ).data.get() @@ -1867,13 +1867,13 @@ def _write_stripped_gpu(path, arr, orientation, extra=None): @pytest.mark.parametrize("orientation", _ORIENTATIONS_GPU) def test_gpu_tiled_matches_cpu_orientation(tmp_path, orientation): """Tiled GPU read of every orientation matches the spec-remapped buffer.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / f"gpu_orient_{orientation}.tif" _write_tiled_gpu(path, arr, orientation) - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) expected = _expected_for_orientation_gpu(arr, orientation) np.testing.assert_array_equal(da.data.get(), expected) @@ -1883,13 +1883,13 @@ def test_gpu_tiled_matches_cpu_orientation(tmp_path, orientation): @pytest.mark.parametrize("orientation", _ORIENTATIONS_GPU) def test_gpu_stripped_matches_cpu_orientation(tmp_path, orientation): """Stripped GPU read also applies orientation (via CPU fallback path).""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(64, dtype=np.uint8).reshape(8, 8) path = tmp_path / f"gpu_strip_orient_{orientation}.tif" _write_stripped_gpu(path, arr, orientation) - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) expected = _expected_for_orientation_gpu(arr, orientation) np.testing.assert_array_equal(da.data.get(), expected) @@ -1901,7 +1901,7 @@ def test_gpu_3band_tiled_matches_cpu_orientation(tmp_path, orientation): """3-band tiled (planar=2) read also applies orientation per band.""" import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu rgb = np.arange(3 * 16 * 16, dtype=np.uint8).reshape(3, 16, 16) path = tmp_path / f"gpu_rgb_orient_{orientation}.tif" @@ -1910,7 +1910,7 @@ def test_gpu_3band_tiled_matches_cpu_orientation(tmp_path, orientation): extratags=[(274, 'H', 1, orientation, True)], ) - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) stored = np.transpose(rgb, (1, 2, 0)) expected = _expected_for_orientation_gpu(stored, orientation) np.testing.assert_array_equal(da.data.get(), expected) @@ -1921,7 +1921,7 @@ def test_gpu_3band_tiled_matches_cpu_orientation(tmp_path, orientation): @pytest.mark.parametrize("orientation", [2, 3, 4]) def test_gpu_orient_2_3_4_coords_track_pixel_flip(tmp_path, orientation): """For mirror-flip orientations, GPU coord array also flips.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / f"gpu_orient_geo_{orientation}.tif" @@ -1940,7 +1940,7 @@ def test_gpu_orient_2_3_4_coords_track_pixel_flip(tmp_path, orientation): with warnings.catch_warnings(): warnings.simplefilter('ignore') - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) targets = [ (100.5, 49.5, 0), @@ -1962,13 +1962,13 @@ def test_gpu_default_orientation_unchanged(tmp_path): """Files without Orientation tag still decode unchanged on GPU.""" import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / "gpu_no_orient.tif" tifffile.imwrite(str(path), arr, tile=(16, 16)) - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) np.testing.assert_array_equal(da.data.get(), arr) @@ -1979,10 +1979,10 @@ def test_gpu_orientation_5_to_8_raise_on_georef(tmp_path, orientation): """GPU reader refuses georef'd files with axis-swap orientations. Mirrors the CPU behaviour added for issue #1765. - ``read_geotiff_gpu`` used to warn and return silently wrong x/y + ``_read_geotiff_gpu`` used to warn and return silently wrong x/y coords for these cases. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / f"gpu_orient_georef_raise_1765_{orientation}.tif" @@ -2000,7 +2000,7 @@ def test_gpu_orientation_5_to_8_raise_on_georef(tmp_path, orientation): ) with pytest.raises(NotImplementedError, match=str(orientation)): - read_geotiff_gpu(str(path)) + _read_geotiff_gpu(str(path)) @_gpu_only @@ -2008,7 +2008,7 @@ def test_gpu_orientation_5_to_8_raise_on_georef(tmp_path, orientation): @pytest.mark.parametrize("orientation", [5, 6, 7, 8]) def test_gpu_orientation_5_to_8_transform_only_raises(tmp_path, orientation): """``has_georef`` without CRS still triggers the raise on GPU.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / f"gpu_orient_transform_only_1765_{orientation}.tif" @@ -2021,7 +2021,7 @@ def test_gpu_orientation_5_to_8_transform_only_raises(tmp_path, orientation): ) with pytest.raises(NotImplementedError, match=str(orientation)): - read_geotiff_gpu(str(path)) + _read_geotiff_gpu(str(path)) @_gpu_only @@ -2029,13 +2029,13 @@ def test_gpu_orientation_5_to_8_transform_only_raises(tmp_path, orientation): @pytest.mark.parametrize("orientation", [5, 6, 7, 8]) def test_gpu_orientation_5_to_8_no_georef_still_swaps(tmp_path, orientation): """Without any geo tags, GPU orientation 5-8 still swaps axes.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(256, dtype=np.uint8).reshape(16, 16) path = tmp_path / f"gpu_orient_no_geo_1765_{orientation}.tif" _write_tiled_gpu(path, arr, orientation) - da = read_geotiff_gpu(str(path)) + da = _read_geotiff_gpu(str(path)) expected = _expected_for_orientation_gpu(arr, orientation) assert da.data.shape == expected.shape np.testing.assert_array_equal(da.data.get(), expected) @@ -2046,10 +2046,10 @@ def test_gpu_orientation_5_to_8_no_georef_still_swaps(tmp_path, orientation): # ===================================================================== # # ``tile_size`` validation on ``to_geotiff`` and ``chunks`` validation -# on ``read_geotiff_dask`` landed first. The matching kwargs +# on ``_read_geotiff_dask`` landed first. The matching kwargs # on three sibling entry points were left unchecked: -# - ``write_geotiff_gpu(tile_size=)`` -# - ``read_geotiff_gpu(chunks=)`` / ``read_vrt(chunks=)`` +# - ``_write_geotiff_gpu(tile_size=)`` +# - ``_read_geotiff_gpu(chunks=)`` / ``_read_vrt(chunks=)`` # # The fix factors the shared validators ``_validate_tile_size_arg`` and # ``_validate_chunks_arg`` and calls them up front from each entry @@ -2074,17 +2074,17 @@ def _make_tif_1776(tmp_path) -> str: def _make_vrt_1776(tmp_path) -> str: """Write a 10x10 GeoTIFF plus a single-source VRT and return the .vrt path.""" - from xrspatial.geotiff import write_vrt + from xrspatial.geotiff import build_vrt tif = _make_tif_1776(tmp_path) vrt = os.path.join(str(tmp_path), 'src_1776.vrt') - write_vrt(vrt, [tif]) + build_vrt(vrt, [tif]) return vrt @_gpu_only class TestWriteGeotiffGpuTileSize_1776: - """Mirror ``test_size_param_validation_1752`` for ``write_geotiff_gpu``.""" + """Mirror ``test_size_param_validation_1752`` for ``_write_geotiff_gpu``.""" @pytest.fixture def gpu_da(self): @@ -2093,206 +2093,206 @@ def gpu_da(self): return xr.DataArray(cupy.asarray(arr), dims=['y', 'x']) def test_tile_size_zero_raises(self, gpu_da, tmp_path): - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') with pytest.raises(ValueError, match='tile_size'): - write_geotiff_gpu(gpu_da, out, tile_size=0) + _write_geotiff_gpu(gpu_da, out, tile_size=0) def test_tile_size_negative_raises(self, gpu_da, tmp_path): - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') with pytest.raises(ValueError, match='tile_size'): - write_geotiff_gpu(gpu_da, out, tile_size=-1) + _write_geotiff_gpu(gpu_da, out, tile_size=-1) def test_tile_size_float_raises(self, gpu_da, tmp_path): - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') with pytest.raises(ValueError, match='tile_size'): - write_geotiff_gpu(gpu_da, out, tile_size=256.0) + _write_geotiff_gpu(gpu_da, out, tile_size=256.0) def test_tile_size_bool_true_raises(self, gpu_da, tmp_path): """``tile_size=True`` is an int subclass that used to silently write a 1x1-tile file. Reject it with a clear ValueError.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') with pytest.raises(ValueError, match='tile_size'): - write_geotiff_gpu(gpu_da, out, tile_size=True) + _write_geotiff_gpu(gpu_da, out, tile_size=True) def test_tile_size_bool_false_raises(self, gpu_da, tmp_path): """``tile_size=False`` was the worst case: ``False == 0`` slipped through the integer check and hit ZeroDivisionError downstream.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') with pytest.raises(ValueError, match='tile_size'): - write_geotiff_gpu(gpu_da, out, tile_size=False) + _write_geotiff_gpu(gpu_da, out, tile_size=False) def test_tile_size_positive_works(self, gpu_da, tmp_path): """tile_size=16 still round-trips.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') - write_geotiff_gpu(gpu_da, out, tile_size=16) + _write_geotiff_gpu(gpu_da, out, tile_size=16) assert os.path.exists(out) def test_tile_size_numpy_int_scalar_works(self, gpu_da, tmp_path): """``np.int64(N)`` is accepted.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = os.path.join(str(tmp_path), 'out_1776.tif') - write_geotiff_gpu(gpu_da, out, tile_size=np.int64(256)) + _write_geotiff_gpu(gpu_da, out, tile_size=np.int64(256)) assert os.path.exists(out) @_gpu_only class TestReadGeotiffGpuChunks_1776: - """Mirror ``test_size_param_validation_1752`` for ``read_geotiff_gpu``.""" + """Mirror ``test_size_param_validation_1752`` for ``_read_geotiff_gpu``.""" def test_chunks_zero_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=0) + _read_geotiff_gpu(path, chunks=0) def test_chunks_negative_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=-1) + _read_geotiff_gpu(path, chunks=-1) def test_chunks_tuple_zero_row_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=(0, 256)) + _read_geotiff_gpu(path, chunks=(0, 256)) def test_chunks_tuple_negative_col_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=(256, -1)) + _read_geotiff_gpu(path, chunks=(256, -1)) def test_chunks_tuple_wrong_length_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=(64, 64, 64)) + _read_geotiff_gpu(path, chunks=(64, 64, 64)) def test_chunks_bool_raises(self, tmp_path): """``chunks=True``/``False`` are int subclasses that used to slip through. Reject them with the same error as a non-int scalar.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=True) + _read_geotiff_gpu(path, chunks=True) def test_chunks_non_int_raises(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks='256') + _read_geotiff_gpu(path, chunks='256') def test_chunks_tuple_float_raises(self, tmp_path): """Tuple entries that aren't int should reject too.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_gpu(path, chunks=(64, 64.5)) + _read_geotiff_gpu(path, chunks=(64, 64.5)) def test_positive_int_chunks_works(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) - r = read_geotiff_gpu(path, chunks=64) + r = _read_geotiff_gpu(path, chunks=64) assert r.shape == (10, 10) def test_positive_tuple_chunks_works(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) - r = read_geotiff_gpu(path, chunks=(4, 8)) + r = _read_geotiff_gpu(path, chunks=(4, 8)) assert r.shape == (10, 10) def test_numpy_int_scalar_chunks_works(self, tmp_path): """``np.int64(N)`` scalar chunk size is accepted.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) - r = read_geotiff_gpu(path, chunks=np.int64(64)) + r = _read_geotiff_gpu(path, chunks=np.int64(64)) assert r.shape == (10, 10) class TestReadVrtChunks_1776: """Same matrix as ``TestReadGeotiffGpuChunks_1776`` but for the VRT - entry point. Runs without CUDA because ``read_vrt(chunks=)`` + entry point. Runs without CUDA because ``_read_vrt(chunks=)`` returns a Dask-backed numpy DataArray; no GPU is required. """ def test_chunks_zero_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=0) + _read_vrt(vrt, chunks=0) def test_chunks_negative_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=-1) + _read_vrt(vrt, chunks=-1) def test_chunks_tuple_zero_row_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=(0, 256)) + _read_vrt(vrt, chunks=(0, 256)) def test_chunks_tuple_negative_col_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=(256, -1)) + _read_vrt(vrt, chunks=(256, -1)) def test_chunks_tuple_wrong_length_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=(64, 64, 64)) + _read_vrt(vrt, chunks=(64, 64, 64)) def test_chunks_bool_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=True) + _read_vrt(vrt, chunks=True) def test_chunks_non_int_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks='256') + _read_vrt(vrt, chunks='256') def test_chunks_tuple_float_raises(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_vrt(vrt, chunks=(64, 64.5)) + _read_vrt(vrt, chunks=(64, 64.5)) def test_positive_int_chunks_works(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) - r = read_vrt(vrt, chunks=64) + r = _read_vrt(vrt, chunks=64) assert r.shape == (10, 10) def test_positive_tuple_chunks_works(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) - r = read_vrt(vrt, chunks=(4, 8)) + r = _read_vrt(vrt, chunks=(4, 8)) assert r.shape == (10, 10) def test_numpy_int_scalar_chunks_works(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) - r = read_vrt(vrt, chunks=np.int64(64)) + r = _read_vrt(vrt, chunks=np.int64(64)) assert r.shape == (10, 10) @_gpu_only class TestOpenGeotiffGpuChunksDispatch_1776: """``open_geotiff(gpu=True, chunks=X)`` routes through - ``read_geotiff_gpu``; pin that the validation propagates through + ``_read_geotiff_gpu``; pin that the validation propagates through the dispatcher. """ @@ -2317,7 +2317,7 @@ def test_open_geotiff_gpu_chunks_tuple_zero_raises(self, tmp_path): class TestToGeotiffGpuTileSizeAlreadyChecked_1776: """``to_geotiff(gpu=True, tile_size=0)`` was already validated by - #1752's CPU-side check before dispatching to ``write_geotiff_gpu``. + #1752's CPU-side check before dispatching to ``_write_geotiff_gpu``. """ @_gpu_only @@ -2342,10 +2342,10 @@ class TestNoDoubleValidationSideEffects_1776: def test_read_geotiff_gpu_chunks_numpy_int_no_side_effect( self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) - r = read_geotiff_gpu(path, chunks=np.int64(64)) + r = _read_geotiff_gpu(path, chunks=np.int64(64)) assert r.shape == (10, 10) out = r.data if hasattr(out, 'compute'): @@ -2353,34 +2353,34 @@ def test_read_geotiff_gpu_chunks_numpy_int_no_side_effect( class TestChunksNoneAcrossEntryPoints_1776: - """``read_geotiff_gpu`` and ``read_vrt`` default to ``chunks=None`` + """``_read_geotiff_gpu`` and ``_read_vrt`` default to ``chunks=None`` (eager read), so the factored ``_validate_chunks_arg`` must let - ``None`` through for them. ``read_geotiff_dask`` does not accept + ``None`` through for them. ``_read_geotiff_dask`` does not accept ``None`` -- its chunk-unpacking math would otherwise fail with a confusing ``TypeError`` instead of a parameter-named ``ValueError``. """ def test_read_geotiff_dask_chunks_none_raises_value_error( self, tmp_path): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path = _make_tif_1776(tmp_path) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=None) + _read_geotiff_dask(path, chunks=None) def test_read_vrt_chunks_none_returns_eager(self, tmp_path): - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt = _make_vrt_1776(tmp_path) - r = read_vrt(vrt, chunks=None) + r = _read_vrt(vrt, chunks=None) assert r.shape == (10, 10) @_gpu_only def test_read_geotiff_gpu_chunks_none_returns_eager(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _make_tif_1776(tmp_path) - r = read_geotiff_gpu(path, chunks=None) + r = _read_geotiff_gpu(path, chunks=None) assert r.shape == (10, 10) @@ -2390,8 +2390,8 @@ def test_read_geotiff_gpu_chunks_none_returns_eager(self, tmp_path): # # The ``mask_nodata`` kwarg is wired through every # public reader entry point in ``xrspatial.geotiff`` -# (``open_geotiff``, ``read_geotiff_gpu``, ``read_geotiff_dask``, -# ``read_vrt``). The original test module +# (``open_geotiff``, ``_read_geotiff_gpu``, ``_read_geotiff_dask``, +# ``_read_vrt``). The original test module # ``test_mask_nodata_kwarg_2052.py`` only exercises eager-numpy and # dask+numpy; this section closes the GPU, dask+GPU, and VRT (eager + # dask) gap. @@ -2426,7 +2426,7 @@ def uint16_with_matching_sentinel_2052(tmp_path): @pytest.fixture def uint16_vrt_with_matching_sentinel_2052(tmp_path): """A single-source VRT wrapping the uint16 fixture above.""" - from xrspatial.geotiff import write_vrt + from xrspatial.geotiff import build_vrt from xrspatial.geotiff._writer import write arr = np.array( @@ -2440,18 +2440,18 @@ def uint16_vrt_with_matching_sentinel_2052(tmp_path): write(arr, tif_path, nodata=0, compression="none", tiled=False) vrt_path = str(tmp_path / "uint16_match_2052.vrt") - write_vrt(vrt_path, [tif_path]) + build_vrt(vrt_path, [tif_path]) return vrt_path, arr @_gpu_only def test_read_geotiff_gpu_mask_nodata_false_preserves_uint16_2052( uint16_with_matching_sentinel_2052): - """``read_geotiff_gpu(mask_nodata=False)`` keeps the uint16 dtype.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(mask_nodata=False)`` keeps the uint16 dtype.""" + from xrspatial.geotiff import _read_geotiff_gpu path, arr = uint16_with_matching_sentinel_2052 - da = read_geotiff_gpu(path, mask_nodata=False) + da = _read_geotiff_gpu(path, mask_nodata=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.data.get(), arr) @@ -2464,10 +2464,10 @@ def test_read_geotiff_gpu_default_mask_nodata_true_still_promotes_2052( """The GPU default is unchanged: ``mask_nodata=True`` promotes.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = uint16_with_matching_sentinel_2052 - da = read_geotiff_gpu(path) + da = _read_geotiff_gpu(path) assert da.dtype == np.float64 nan_count = int(cupy.isnan(da.data).sum().get()) @@ -2493,10 +2493,10 @@ def test_open_geotiff_gpu_mask_nodata_false_threads_through_2052( def test_read_geotiff_gpu_dtype_uint16_with_mask_nodata_false_2052( uint16_with_matching_sentinel_2052): """``dtype="uint16"`` works on the GPU branch when the opt-out is set.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = uint16_with_matching_sentinel_2052 - da = read_geotiff_gpu(path, dtype="uint16", mask_nodata=False) + da = _read_geotiff_gpu(path, dtype="uint16", mask_nodata=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.data.get(), arr) @@ -2505,11 +2505,11 @@ def test_read_geotiff_gpu_dtype_uint16_with_mask_nodata_false_2052( @_gpu_only def test_read_geotiff_gpu_dask_mask_nodata_false_preserves_uint16_2052( uint16_with_matching_sentinel_2052): - """``read_geotiff_gpu(chunks=..., mask_nodata=False)`` keeps uint16.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(chunks=..., mask_nodata=False)`` keeps uint16.""" + from xrspatial.geotiff import _read_geotiff_gpu path, arr = uint16_with_matching_sentinel_2052 - da = read_geotiff_gpu(path, chunks=2, mask_nodata=False) + da = _read_geotiff_gpu(path, chunks=2, mask_nodata=False) assert da.dtype == np.uint16 @@ -2524,10 +2524,10 @@ def test_read_geotiff_gpu_dask_default_mask_nodata_true_still_promotes_2052( """The dask+GPU default still promotes the graph dtype to float64.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = uint16_with_matching_sentinel_2052 - da = read_geotiff_gpu(path, chunks=2) + da = _read_geotiff_gpu(path, chunks=2) assert da.dtype == np.float64 computed = da.compute() @@ -2553,12 +2553,12 @@ def test_open_geotiff_dask_gpu_mask_nodata_false_threads_through_2052( def test_read_vrt_mask_nodata_false_preserves_uint16_2052( uint16_vrt_with_matching_sentinel_2052): - """``read_vrt(mask_nodata=False)`` keeps the uint16 dtype on the + """``_read_vrt(mask_nodata=False)`` keeps the uint16 dtype on the eager VRT path.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path, arr = uint16_vrt_with_matching_sentinel_2052 - da = read_vrt(vrt_path, mask_nodata=False) + da = _read_vrt(vrt_path, mask_nodata=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(np.asarray(da.values), arr) @@ -2568,10 +2568,10 @@ def test_read_vrt_mask_nodata_false_preserves_uint16_2052( def test_read_vrt_default_mask_nodata_true_still_promotes_2052( uint16_vrt_with_matching_sentinel_2052): """The VRT default unchanged: ``mask_nodata=True`` promotes.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path, _ = uint16_vrt_with_matching_sentinel_2052 - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) assert da.dtype == np.float64 assert int(np.isnan(da.values).sum()) == len(_SENTINEL_POS_2052) @@ -2593,10 +2593,10 @@ def test_open_geotiff_vrt_mask_nodata_false_threads_through_2052( def test_read_vrt_dtype_uint16_with_mask_nodata_false_2052( uint16_vrt_with_matching_sentinel_2052): """``dtype="uint16"`` works on the VRT path with the opt-out.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path, arr = uint16_vrt_with_matching_sentinel_2052 - da = read_vrt(vrt_path, dtype="uint16", mask_nodata=False) + da = _read_vrt(vrt_path, dtype="uint16", mask_nodata=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(np.asarray(da.values), arr) @@ -2604,12 +2604,12 @@ def test_read_vrt_dtype_uint16_with_mask_nodata_false_2052( def test_read_vrt_chunked_mask_nodata_false_preserves_uint16_2052( uint16_vrt_with_matching_sentinel_2052): - """``read_vrt(chunks=..., mask_nodata=False)`` keeps uint16 on + """``_read_vrt(chunks=..., mask_nodata=False)`` keeps uint16 on the dask+VRT path.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path, arr = uint16_vrt_with_matching_sentinel_2052 - da = read_vrt(vrt_path, chunks=2, mask_nodata=False) + da = _read_vrt(vrt_path, chunks=2, mask_nodata=False) assert da.dtype == np.uint16 computed = da.compute() @@ -2620,10 +2620,10 @@ def test_read_vrt_chunked_mask_nodata_false_preserves_uint16_2052( def test_read_vrt_chunked_default_mask_nodata_true_still_promotes_2052( uint16_vrt_with_matching_sentinel_2052): """The chunked-VRT default still promotes to float64.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path, _ = uint16_vrt_with_matching_sentinel_2052 - da = read_vrt(vrt_path, chunks=2) + da = _read_vrt(vrt_path, chunks=2) assert da.dtype == np.float64 computed = da.compute() @@ -2715,11 +2715,11 @@ def test_cross_backend_parity_eager_vrt_2052( def test_read_geotiff_dask_direct_mask_nodata_false_2052( uint16_with_matching_sentinel_2052): - """The direct ``read_geotiff_dask`` entry point also accepts the kwarg.""" - from xrspatial.geotiff import read_geotiff_dask + """The direct ``_read_geotiff_dask`` entry point also accepts the kwarg.""" + from xrspatial.geotiff import _read_geotiff_dask path, arr = uint16_with_matching_sentinel_2052 - da = read_geotiff_dask(path, chunks=2, mask_nodata=False) + da = _read_geotiff_dask(path, chunks=2, mask_nodata=False) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.compute().values, arr) @@ -2731,9 +2731,9 @@ def test_read_geotiff_dask_direct_mask_nodata_false_2052( # # ``open_geotiff(gpu=True)`` used to silently drop the # ``on_gpu_failure`` kwarg: the dispatcher did not declare it and the -# GPU branch did not forward it to ``read_geotiff_gpu``. Callers that +# GPU branch did not forward it to ``_read_geotiff_gpu``. Callers that # wanted strict GPU failure semantics had to bypass ``open_geotiff`` -# entirely and call ``read_geotiff_gpu`` directly, defeating the +# entirely and call ``_read_geotiff_gpu`` directly, defeating the # dispatcher. @@ -2851,9 +2851,9 @@ def test_open_geotiff_gpu_rejects_invalid_on_gpu_failure_1615( def test_invalid_on_gpu_failure_reaches_gpu_validator_on_cpu_1615( small_tiff_path_1615): """Even on a CPU-only host, the kwarg should reach - ``read_geotiff_gpu``. + ``_read_geotiff_gpu``. - Without GPU hardware, ``read_geotiff_gpu`` raises ``ImportError`` + Without GPU hardware, ``_read_geotiff_gpu`` raises ``ImportError`` (no cupy) or runs through to the actual decode. The kwarg validator runs before the cupy import, so an invalid value surfaces deterministically in both environments. This pins the @@ -2871,7 +2871,7 @@ def test_invalid_on_gpu_failure_reaches_gpu_validator_on_cpu_1615( # ===================================================================== # # ``_validate_crs_fallback`` wires ``allow_unparseable_crs`` into -# ``to_geotiff``, ``write_geotiff_gpu``, and the ``to_geotiff(gpu=True)`` +# ``to_geotiff``, ``_write_geotiff_gpu``, and the ``to_geotiff(gpu=True)`` # dispatcher. ``test_crs_fail_closed_1929`` only exercises the eager # CPU writer; this section closes the GPU and dispatcher gap. @@ -2900,36 +2900,36 @@ def _make_cpu_da_1929() -> xr.DataArray: @_gpu_only class TestWriteGeotiffGpuFailClosed_1929: - """``write_geotiff_gpu`` refuses to land an unvalidatable CRS by default.""" + """``_write_geotiff_gpu`` refuses to land an unvalidatable CRS by default.""" def test_garbage_string_kwarg_raises(self, tmp_path): """A free-form non-WKT, non-PROJ string raises by default.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_garbage_kwarg_1929.tif") with pytest.warns(Warning): with pytest.raises(ValueError, match="GTCitationGeoKey"): - write_geotiff_gpu( + _write_geotiff_gpu( _make_gpu_da_1929(), out, crs="absolute-garbage") def test_garbage_string_attr_raises(self, tmp_path): """Same guard fires when garbage arrives via ``attrs['crs']``.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_garbage_attr_1929.tif") da = _make_gpu_da_1929() da.attrs["crs"] = "still-garbage" with pytest.warns(Warning): with pytest.raises(ValueError, match="GTCitationGeoKey"): - write_geotiff_gpu(da, out) + _write_geotiff_gpu(da, out) def test_opt_in_allows_garbage(self, tmp_path): """``allow_unparseable_crs=True`` restores the citation-only write.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_optin_1929.tif") with pytest.warns(Warning): - write_geotiff_gpu( + _write_geotiff_gpu( _make_gpu_da_1929(), out, crs="absolute-garbage", @@ -2939,12 +2939,12 @@ def test_opt_in_allows_garbage(self, tmp_path): def test_message_recommends_alternatives(self, tmp_path): """The error message points to the four recovery options.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_msg_check_1929.tif") with pytest.warns(Warning): with pytest.raises(ValueError) as exc: - write_geotiff_gpu(_make_gpu_da_1929(), out, crs="bogus") + _write_geotiff_gpu(_make_gpu_da_1929(), out, crs="bogus") msg = str(exc.value) assert "EPSG" in msg assert "WKT" in msg @@ -2952,15 +2952,15 @@ def test_message_recommends_alternatives(self, tmp_path): def test_epsg_int_unchanged(self, tmp_path): """An int EPSG kwarg never reaches the fallback path.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_epsg_int_1929.tif") - write_geotiff_gpu(_make_gpu_da_1929(), out, crs=4326) + _write_geotiff_gpu(_make_gpu_da_1929(), out, crs=4326) assert os.path.exists(out) def test_valid_wkt_unchanged(self, tmp_path): """A WKT-shaped string is accepted by the structural check.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_wkt_shaped_1929.tif") wkt = ( @@ -2968,15 +2968,15 @@ def test_valid_wkt_unchanged(self, tmp_path): "6378137,298.257223563]],PRIMEM[\"Greenwich\",0]," "UNIT[\"degree\",0.0174532925199433]]" ) - write_geotiff_gpu(_make_gpu_da_1929(), out, crs=wkt) + _write_geotiff_gpu(_make_gpu_da_1929(), out, crs=wkt) assert os.path.exists(out) def test_no_crs_at_all_unchanged(self, tmp_path): """No CRS supplied means no GTCitationGeoKey.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu out = str(tmp_path / "gpu_no_crs_1929.tif") - write_geotiff_gpu(_make_gpu_da_1929(), out) + _write_geotiff_gpu(_make_gpu_da_1929(), out) assert os.path.exists(out) @@ -2986,17 +2986,17 @@ class TestToGeotiffGpuDispatcherFailClosed_1929: @staticmethod def _install_gpu_spy(monkeypatch): - """Wrap ``write_geotiff_gpu`` so the dispatcher entry is recorded.""" + """Wrap ``_write_geotiff_gpu`` so the dispatcher entry is recorded.""" from xrspatial.geotiff._writers import eager as _eager - real = _eager.write_geotiff_gpu + real = _eager._write_geotiff_gpu calls = [] def _spy(*args, **kwargs): calls.append((args, kwargs)) return real(*args, **kwargs) - monkeypatch.setattr(_eager, "write_geotiff_gpu", _spy) + monkeypatch.setattr(_eager, "_write_geotiff_gpu", _spy) return calls def test_dispatcher_garbage_raises_with_cupy_input( @@ -3078,7 +3078,7 @@ class TestErrorMessageParity_1929: """ def test_gpu_vs_cpu_message_matches(self, tmp_path): - from xrspatial.geotiff import to_geotiff, write_geotiff_gpu + from xrspatial.geotiff import to_geotiff, _write_geotiff_gpu out_cpu = str(tmp_path / "cpu_msg_1929.tif") out_gpu = str(tmp_path / "gpu_msg_1929.tif") @@ -3089,7 +3089,7 @@ def test_gpu_vs_cpu_message_matches(self, tmp_path): to_geotiff( _make_cpu_da_1929(), out_cpu, crs="absolute-garbage") with pytest.raises(ValueError) as exc_gpu: - write_geotiff_gpu( + _write_geotiff_gpu( _make_gpu_da_1929(), out_gpu, crs="absolute-garbage") msg_cpu = str(exc_cpu.value) @@ -3121,10 +3121,10 @@ class TestKwargDefaultParity_1929: """ def test_default_is_false_on_all_writers(self): - from xrspatial.geotiff import to_geotiff, write_geotiff_gpu + from xrspatial.geotiff import to_geotiff, _write_geotiff_gpu from xrspatial.geotiff._writers.eager import _write_vrt_tiled - for fn in (to_geotiff, write_geotiff_gpu, _write_vrt_tiled): + for fn in (to_geotiff, _write_geotiff_gpu, _write_vrt_tiled): sig = inspect.signature(fn) param = sig.parameters.get("allow_unparseable_crs") assert param is not None, ( diff --git a/xrspatial/geotiff/tests/gpu/test_reader.py b/xrspatial/geotiff/tests/gpu/test_reader.py index 2f625973c..7608fd3ab 100644 --- a/xrspatial/geotiff/tests/gpu/test_reader.py +++ b/xrspatial/geotiff/tests/gpu/test_reader.py @@ -8,15 +8,15 @@ Sections, by the behaviour they pin: -- ``read_geotiff_gpu(..., window=..., band=...)`` kwarg forwarding +- ``_read_geotiff_gpu(..., window=..., band=...)`` kwarg forwarding - ``stripped multiband`` -- 3-D ``(y, x, band)`` reads via the stripped fallback - stripped no-georef windowed coord parity - stripped fallback forwards ``max_pixels`` / ``window`` / ``band`` - GPU read nodata promotion + ``attrs['nodata']`` -- ``write_geotiff_gpu`` rejects MinIsWhite on band-first single-band -- ``read_geotiff_gpu(chunks=...)`` is a real out-of-core dask graph +- ``_write_geotiff_gpu`` rejects MinIsWhite on band-first single-band +- ``_read_geotiff_gpu(chunks=...)`` is a real out-of-core dask graph - sidecar overview-inheritance georef parity across backends -- ``read_geotiff_gpu`` accepts HTTP / fsspec URLs on the eager path +- ``_read_geotiff_gpu`` accepts HTTP / fsspec URLs on the eager path - GDS chunked path casts each chunk to the declared dtype Markers come from ``_helpers/markers.py``. Every test carries @@ -90,12 +90,12 @@ def multi_band_tiff_1605(tmp_path): def test_read_geotiff_gpu_window_matches_eager_1605(single_band_tiff_1605): """Direct call: GPU window slice matches CPU eager window slice.""" path, source_arr = single_band_tiff_1605 - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu window = (2, 4, 12, 14) cpu = open_geotiff(path, window=window) - gpu = read_geotiff_gpu(path, window=window) + gpu = _read_geotiff_gpu(path, window=window) assert gpu.shape == cpu.shape == (10, 10) np.testing.assert_array_equal(gpu.data.get(), cpu.data) @@ -126,10 +126,10 @@ def test_open_geotiff_gpu_window_no_longer_silently_dropped_1605( def test_read_geotiff_gpu_band_selection_1605(multi_band_tiff_1605): """Direct call: band=k returns the kth band as a 2D DataArray.""" path, source_arr = multi_band_tiff_1605 - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu cpu = open_geotiff(path, band=1) - gpu = read_geotiff_gpu(path, band=1) + gpu = _read_geotiff_gpu(path, band=1) assert gpu.shape == cpu.shape == (16, 20) assert gpu.ndim == 2 @@ -158,11 +158,11 @@ def test_open_geotiff_gpu_band_no_longer_silently_dropped_1605( def test_read_geotiff_gpu_window_and_band_1605(multi_band_tiff_1605): """window + band combine cleanly.""" path, source_arr = multi_band_tiff_1605 - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu window = (1, 2, 11, 17) cpu = open_geotiff(path, window=window, band=0) - gpu = read_geotiff_gpu(path, window=window, band=0) + gpu = _read_geotiff_gpu(path, window=window, band=0) assert gpu.shape == cpu.shape == (10, 15) assert gpu.ndim == 2 @@ -173,16 +173,16 @@ def test_read_geotiff_gpu_window_and_band_1605(multi_band_tiff_1605): def test_read_geotiff_gpu_window_bounds_validation_1605(single_band_tiff_1605): """Out-of-bounds window raises ValueError, mirroring the dask path.""" path, _ = single_band_tiff_1605 - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(ValueError, match="outside the source extent"): - read_geotiff_gpu(path, window=(0, 0, 100, 100)) + _read_geotiff_gpu(path, window=(0, 0, 100, 100)) with pytest.raises(ValueError, match="non-positive size"): - read_geotiff_gpu(path, window=(5, 0, 5, 10)) + _read_geotiff_gpu(path, window=(5, 0, 5, 10)) with pytest.raises(ValueError, match="must be a 4-tuple"): - read_geotiff_gpu(path, window=(0, 0, 5)) + _read_geotiff_gpu(path, window=(0, 0, 5)) @_gpu_only @@ -191,13 +191,13 @@ def test_read_geotiff_gpu_band_bounds_validation_1605( """Out-of-range band raises IndexError.""" multi_path, _ = multi_band_tiff_1605 single_path, _ = single_band_tiff_1605 - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(IndexError, match="out of range"): - read_geotiff_gpu(multi_path, band=10) + _read_geotiff_gpu(multi_path, band=10) with pytest.raises(IndexError, match="single-band file"): - read_geotiff_gpu(single_path, band=1) + _read_geotiff_gpu(single_path, band=1) @_gpu_only @@ -211,7 +211,7 @@ def test_read_geotiff_gpu_window_rejected_on_nondefault_orientation_1605( tifffile. """ tifffile = pytest.importorskip("tifffile") - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(16 * 20, dtype=np.float32).reshape(16, 20) path = tmp_path / "orient2_stripped_1605.tif" @@ -222,7 +222,7 @@ def test_read_geotiff_gpu_window_rejected_on_nondefault_orientation_1605( ) with pytest.raises(ValueError, match=r"Orientation tag \(274\) is 2"): - read_geotiff_gpu(str(path), window=(0, 0, 8, 10)) + _read_geotiff_gpu(str(path), window=(0, 0, 8, 10)) @_gpu_only @@ -237,14 +237,14 @@ def test_read_geotiff_gpu_stripped_chunks_produces_dask_1605(tmp_path): tifffile = pytest.importorskip("tifffile") import dask.array as dask_array - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(16 * 20, dtype=np.float32).reshape(16, 20) path = tmp_path / "stripped_chunks_1605.tif" # No ``tile=`` argument -> stripped layout. Orientation defaults to 1. tifffile.imwrite(str(path), arr) - result = read_geotiff_gpu(str(path), chunks=8) + result = _read_geotiff_gpu(str(path), chunks=8) # The result should be Dask-backed; .data is a dask Array wrapping CuPy. assert isinstance(result.data, dask_array.Array), \ @@ -264,13 +264,13 @@ def test_read_geotiff_gpu_stripped_chunks_tuple_1605(tmp_path): tifffile = pytest.importorskip("tifffile") import dask.array as dask_array - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.arange(16 * 20, dtype=np.float32).reshape(16, 20) path = tmp_path / "stripped_chunks_tuple_1605.tif" tifffile.imwrite(str(path), arr) - result = read_geotiff_gpu(str(path), chunks=(4, 10)) + result = _read_geotiff_gpu(str(path), chunks=(4, 10)) assert isinstance(result.data, dask_array.Array) assert result.chunks == ((4, 4, 4, 4), (10, 10)) @@ -282,7 +282,7 @@ def test_read_geotiff_gpu_stripped_chunks_tuple_1605(tmp_path): @_gpu_only def test_stripped_3band_uint8_stripped_multiband(): """3-band uint8 stripped TIFF reads as (y, x, band).""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260508) data = rng.randint(0, 200, size=(64, 96, 3)).astype(np.uint8) @@ -291,7 +291,7 @@ def test_stripped_3band_uint8_stripped_multiband(): with tempfile.TemporaryDirectory() as d: p = os.path.join(d, 'wt.tif') to_geotiff(da, p, tiled=False) - out = read_geotiff_gpu(p) + out = _read_geotiff_gpu(p) assert out.dims == ('y', 'x', 'band') assert out.shape == (64, 96, 3) np.testing.assert_array_equal(out.data.get(), data) @@ -300,7 +300,7 @@ def test_stripped_3band_uint8_stripped_multiband(): @_gpu_only def test_stripped_2band_uint16_stripped_multiband(): """2-band uint16 stripped TIFF reads as (y, x, band).""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260508) data = rng.randint(0, 60000, size=(48, 80, 2)).astype(np.uint16) @@ -309,7 +309,7 @@ def test_stripped_2band_uint16_stripped_multiband(): with tempfile.TemporaryDirectory() as d: p = os.path.join(d, 'wt2.tif') to_geotiff(da, p, tiled=False) - out = read_geotiff_gpu(p) + out = _read_geotiff_gpu(p) assert out.dims == ('y', 'x', 'band') assert out.shape == (48, 80, 2) assert out.data.dtype == np.dtype(np.uint16) @@ -319,7 +319,7 @@ def test_stripped_2band_uint16_stripped_multiband(): @_gpu_only def test_stripped_singleband_still_2d_stripped_multiband(): """Single-band stripped TIFF still produces a 2-D (y, x) DataArray.""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260508) data = rng.randint(0, 200, size=(40, 60)).astype(np.uint8) @@ -328,7 +328,7 @@ def test_stripped_singleband_still_2d_stripped_multiband(): with tempfile.TemporaryDirectory() as d: p = os.path.join(d, 'wt1.tif') to_geotiff(da, p, tiled=False) - out = read_geotiff_gpu(p) + out = _read_geotiff_gpu(p) assert out.dims == ('y', 'x') assert out.shape == (40, 60) np.testing.assert_array_equal(out.data.get(), data) @@ -511,7 +511,7 @@ def test_georef_offset_window(self, tmp_path): @_gpu_only def test_stripped_max_pixels_cap_is_enforced_1732(): """max_pixels smaller than the file must raise before full decode.""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260512) data = rng.randint(0, 200, size=(64, 96)).astype(np.uint8) @@ -522,14 +522,14 @@ def test_stripped_max_pixels_cap_is_enforced_1732(): to_geotiff(da, p, tiled=False) # 64 * 96 = 6144 pixels; cap at 1000 must reject. with pytest.raises(ValueError, match="max_pixels|pixel"): - read_geotiff_gpu(p, max_pixels=1000) + _read_geotiff_gpu(p, max_pixels=1000) @_gpu_only def test_stripped_window_returns_only_window_1732(): """Windowed read on a stripped file returns the window-sized array with coords and transform that match the window origin.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260512) data = rng.randint(0, 200, size=(64, 96)).astype(np.uint8) @@ -547,7 +547,7 @@ def test_stripped_window_returns_only_window_1732(): p = os.path.join(d, 'tmp_1732_win.tif') to_geotiff(da, p, tiled=False) win = (8, 16, 40, 80) # 32x64 window - out = read_geotiff_gpu(p, window=win) + out = _read_geotiff_gpu(p, window=win) assert out.shape == (32, 64) np.testing.assert_array_equal(out.data.get(), data[8:40, 16:80]) @@ -565,7 +565,7 @@ def test_stripped_window_returns_only_window_1732(): def test_stripped_band_selection_returns_2d_1732(): """Selecting band=1 on a 3-band stripped file returns a 2D array matching the requested band.""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260512) data = rng.randint(0, 200, size=(48, 80, 3)).astype(np.uint8) @@ -574,7 +574,7 @@ def test_stripped_band_selection_returns_2d_1732(): with tempfile.TemporaryDirectory() as d: p = os.path.join(d, 'tmp_1732_band.tif') to_geotiff(da, p, tiled=False) - out = read_geotiff_gpu(p, band=1) + out = _read_geotiff_gpu(p, band=1) assert out.dims == ('y', 'x') assert out.shape == (48, 80) np.testing.assert_array_equal(out.data.get(), data[:, :, 1]) @@ -583,7 +583,7 @@ def test_stripped_band_selection_returns_2d_1732(): @_gpu_only def test_stripped_window_plus_band_1732(): """Windowed read with band selection composes correctly.""" - from xrspatial.geotiff import read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(20260512) data = rng.randint(0, 200, size=(48, 80, 3)).astype(np.uint8) @@ -593,7 +593,7 @@ def test_stripped_window_plus_band_1732(): p = os.path.join(d, 'tmp_1732_wb.tif') to_geotiff(da, p, tiled=False) win = (4, 8, 36, 72) # 32x64 - out = read_geotiff_gpu(p, window=win, band=2) + out = _read_geotiff_gpu(p, window=win, band=2) assert out.dims == ('y', 'x') assert out.shape == (32, 64) np.testing.assert_array_equal( @@ -759,7 +759,7 @@ def test_gpu_int16_negative_nodata_1542(tmp_path): # --------------------------------------------------------------------------- -# Section: write_geotiff_gpu rejects MinIsWhite (band-first guard) +# Section: _write_geotiff_gpu rejects MinIsWhite (band-first guard) # # The last test in this section # (``test_samples_hint_band_first_without_gpu_2097``) is CPU-only by design: @@ -774,13 +774,13 @@ def test_band_first_single_band_miniswhite_rejected_2097(tmp_path): must raise ``NotImplementedError`` on the GPU writer.""" import cupy as cp - from xrspatial.geotiff._writers.gpu import write_geotiff_gpu + from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu arr = cp.zeros((1, 4, 8), dtype=cp.uint8) da = xr.DataArray(arr, dims=("band", "y", "x")) out = tmp_path / "tmp_2097_miniswhite_band_first.tif" with pytest.raises(NotImplementedError, match="miniswhite"): - write_geotiff_gpu(da, str(out), photometric="miniswhite") + _write_geotiff_gpu(da, str(out), photometric="miniswhite") assert not out.exists() @@ -789,13 +789,13 @@ def test_band_last_single_band_miniswhite_still_rejected_2097(tmp_path): """The pre-existing band-last single-band rejection must still fire.""" import cupy as cp - from xrspatial.geotiff._writers.gpu import write_geotiff_gpu + from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu arr = cp.zeros((4, 8, 1), dtype=cp.uint8) da = xr.DataArray(arr, dims=("y", "x", "band")) out = tmp_path / "tmp_2097_miniswhite_band_last.tif" with pytest.raises(NotImplementedError, match="miniswhite"): - write_geotiff_gpu(da, str(out), photometric="miniswhite") + _write_geotiff_gpu(da, str(out), photometric="miniswhite") assert not out.exists() @@ -804,13 +804,13 @@ def test_2d_single_band_miniswhite_still_rejected_2097(tmp_path): """2D inputs are the simplest single-band case.""" import cupy as cp - from xrspatial.geotiff._writers.gpu import write_geotiff_gpu + from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu arr = cp.zeros((4, 8), dtype=cp.uint8) da = xr.DataArray(arr, dims=("y", "x")) out = tmp_path / "tmp_2097_miniswhite_2d.tif" with pytest.raises(NotImplementedError, match="miniswhite"): - write_geotiff_gpu(da, str(out), photometric="miniswhite") + _write_geotiff_gpu(da, str(out), photometric="miniswhite") assert not out.exists() @@ -844,7 +844,7 @@ def test_samples_hint_band_first_without_gpu_2097(): # --------------------------------------------------------------------------- -# Section: read_geotiff_gpu(chunks=...) out-of-core dask pipeline +# Section: _read_geotiff_gpu(chunks=...) out-of-core dask pipeline # --------------------------------------------------------------------------- def _kvikio_available_1876() -> bool: @@ -894,9 +894,9 @@ def test_read_geotiff_gpu_chunks_yields_dask_cupy_chunks_1876( import cupy import dask.array as da_mod - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - result = read_geotiff_gpu(small_raster_path_1876, chunks=8) + result = _read_geotiff_gpu(small_raster_path_1876, chunks=8) assert isinstance(result.data, da_mod.Array), ( f"expected dask Array, got {type(result.data).__name__}" @@ -917,10 +917,10 @@ def test_read_geotiff_gpu_chunks_values_match_eager_1876( """Lazy chunked result must equal the eager GPU result element-wise.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - eager = read_geotiff_gpu(small_raster_path_1876) - chunked = read_geotiff_gpu(small_raster_path_1876, chunks=8) + eager = _read_geotiff_gpu(small_raster_path_1876) + chunked = _read_geotiff_gpu(small_raster_path_1876, chunks=8) eager_np = cupy.asnumpy(eager.data) chunked_np = cupy.asnumpy(chunked.compute().data) @@ -933,9 +933,9 @@ def test_read_geotiff_gpu_no_chunks_returns_eager_cupy_1876( """``chunks=None`` keeps the eager GPU decode path.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - result = read_geotiff_gpu(small_raster_path_1876) + result = _read_geotiff_gpu(small_raster_path_1876) assert isinstance(result.data, cupy.ndarray) @@ -959,9 +959,9 @@ def test_open_geotiff_gpu_chunks_propagates_to_dask_1876( def test_read_geotiff_gpu_chunks_preserves_attrs_1876( small_raster_path_1876): """Geo attrs (transform, crs) must survive the dask path.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - result = read_geotiff_gpu(small_raster_path_1876, chunks=8) + result = _read_geotiff_gpu(small_raster_path_1876, chunks=8) assert 'transform' in result.attrs assert 'crs' in result.attrs @@ -972,8 +972,8 @@ def test_read_geotiff_gpu_chunks_uses_gds_path_when_available_1876( small_raster_path_1876, monkeypatch): """When kvikio is installed and the file qualifies, each chunk task must call the direct disk->GPU decoder rather than detouring through - ``read_geotiff_dask``.""" - from xrspatial.geotiff import read_geotiff_gpu + ``_read_geotiff_dask``.""" + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gtmod direct_calls = {'n': 0} @@ -985,7 +985,7 @@ def _spy(*args, **kwargs): monkeypatch.setattr(gtmod, '_decode_window_gpu_direct', _spy) - result = read_geotiff_gpu(small_raster_path_1876, chunks=8) + result = _read_geotiff_gpu(small_raster_path_1876, chunks=8) result.compute() assert direct_calls['n'] == 16, ( @@ -1000,11 +1000,11 @@ def test_read_geotiff_gpu_chunks_window_subset_1876(small_raster_path_1876): on the eager path.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - eager = read_geotiff_gpu(small_raster_path_1876, window=(4, 4, 24, 28)) - chunked = read_geotiff_gpu(small_raster_path_1876, chunks=8, - window=(4, 4, 24, 28)) + eager = _read_geotiff_gpu(small_raster_path_1876, window=(4, 4, 24, 28)) + chunked = _read_geotiff_gpu(small_raster_path_1876, chunks=8, + window=(4, 4, 24, 28)) eager_np = cupy.asnumpy(eager.data) chunked_np = cupy.asnumpy(chunked.compute().data) @@ -1018,14 +1018,14 @@ def test_read_geotiff_gpu_chunks_multi_band_1876(multi_band_path_1876): import cupy import dask.array as da_mod - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - result = read_geotiff_gpu(multi_band_path_1876, chunks=16) + result = _read_geotiff_gpu(multi_band_path_1876, chunks=16) assert isinstance(result.data, da_mod.Array) assert isinstance(result.data._meta, cupy.ndarray) assert result.sizes['band'] == 3 - eager = read_geotiff_gpu(multi_band_path_1876) + eager = _read_geotiff_gpu(multi_band_path_1876) eager_np = cupy.asnumpy(eager.data) chunked_np = cupy.asnumpy(result.compute().data) np.testing.assert_allclose(eager_np, chunked_np, rtol=1e-5) @@ -1037,13 +1037,13 @@ def test_read_geotiff_gpu_chunks_single_band_selection_1876( """``band=k`` collapses to a 2D Dask+CuPy DataArray.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - result = read_geotiff_gpu(multi_band_path_1876, chunks=16, band=1) + result = _read_geotiff_gpu(multi_band_path_1876, chunks=16, band=1) assert result.ndim == 2 assert isinstance(result.data._meta, cupy.ndarray) - eager = read_geotiff_gpu(multi_band_path_1876, band=1) + eager = _read_geotiff_gpu(multi_band_path_1876, band=1) eager_np = cupy.asnumpy(eager.data) chunked_np = cupy.asnumpy(result.compute().data) np.testing.assert_allclose(eager_np, chunked_np, rtol=1e-5) @@ -1058,7 +1058,7 @@ def test_read_geotiff_gpu_chunks_fallback_when_kvikio_absent_1876( import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._backends import gpu as gtmod original_find_spec = _ilu.find_spec @@ -1079,12 +1079,12 @@ def _spy(*args, **kwargs): monkeypatch.setattr(gtmod, '_decode_window_gpu_direct', _spy) - result = read_geotiff_gpu(small_raster_path_1876, chunks=8) + result = _read_geotiff_gpu(small_raster_path_1876, chunks=8) computed = result.compute() assert direct_calls['n'] == 0 assert isinstance(computed.data, cupy.ndarray) - eager = read_geotiff_gpu(small_raster_path_1876) + eager = _read_geotiff_gpu(small_raster_path_1876) np.testing.assert_array_equal( cupy.asnumpy(eager.data), cupy.asnumpy(computed.data), ) @@ -1105,7 +1105,7 @@ def _attrs_subset_2324(da): def test_sidecar_without_geokeys_attrs_match_cpu_vs_dask_2324(tmp_path): """Baseline: CPU eager and dask agree on inherited georef.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_dask + from xrspatial.geotiff import open_geotiff, _read_geotiff_dask from ..integration.test_sidecar import _write_pair @@ -1121,7 +1121,7 @@ def test_sidecar_without_geokeys_attrs_match_cpu_vs_dask_2324(tmp_path): ) cpu = open_geotiff(str(base), overview_level=1) - dsk = read_geotiff_dask(str(base), overview_level=1, chunks=2) + dsk = _read_geotiff_dask(str(base), overview_level=1, chunks=2) assert _attrs_subset_2324(cpu) == _attrs_subset_2324(dsk) t = cpu.attrs["transform"] @@ -1135,7 +1135,7 @@ def test_sidecar_without_geokeys_attrs_match_cpu_vs_dask_2324(tmp_path): @_gpu_only def test_sidecar_without_geokeys_gpu_matches_cpu_2324(tmp_path): """GPU eager georef matches CPU / dask.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu from ..integration.test_sidecar import _write_pair @@ -1151,7 +1151,7 @@ def test_sidecar_without_geokeys_gpu_matches_cpu_2324(tmp_path): ) cpu = open_geotiff(str(base), overview_level=1) - gpu = read_geotiff_gpu(str(base), overview_level=1) + gpu = _read_geotiff_gpu(str(base), overview_level=1) assert _attrs_subset_2324(gpu) == _attrs_subset_2324(cpu) np.testing.assert_array_equal(np.asarray(gpu.data.get()), @@ -1161,7 +1161,7 @@ def test_sidecar_without_geokeys_gpu_matches_cpu_2324(tmp_path): @_gpu_only def test_sidecar_with_own_geokeys_gpu_matches_cpu_2324(tmp_path): """GPU path routes a sidecar-owned georef payload to sidecar bytes.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu from ..integration.test_sidecar import _write_pair @@ -1177,7 +1177,7 @@ def test_sidecar_with_own_geokeys_gpu_matches_cpu_2324(tmp_path): ) cpu = open_geotiff(str(base), overview_level=1) - gpu = read_geotiff_gpu(str(base), overview_level=1) + gpu = _read_geotiff_gpu(str(base), overview_level=1) assert _attrs_subset_2324(gpu) == _attrs_subset_2324(cpu) t = gpu.attrs["transform"] @@ -1189,7 +1189,7 @@ def test_sidecar_with_own_geokeys_gpu_matches_cpu_2324(tmp_path): # --------------------------------------------------------------------------- -# Section: read_geotiff_gpu accepts HTTP / fsspec URLs (eager) +# Section: _read_geotiff_gpu accepts HTTP / fsspec URLs (eager) # --------------------------------------------------------------------------- class _RangeHandler2161(http.server.BaseHTTPRequestHandler): @@ -1247,14 +1247,14 @@ def small_tif_bytes_2161(tmp_path): @_gpu_only def test_local_path_still_returns_cupy_2161(small_tif_bytes_2161): - """``read_geotiff_gpu(local.tif)`` must still take the disk-based + """``_read_geotiff_gpu(local.tif)`` must still take the disk-based eager path and return a CuPy-backed DataArray.""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu _payload, arr_ref, local = small_tif_bytes_2161 - da = read_geotiff_gpu(local) + da = _read_geotiff_gpu(local) assert isinstance(da.data, cupy.ndarray), ( f"Local path no longer returns CuPy array (got {type(da.data)})" ) @@ -1268,7 +1268,7 @@ def test_http_url_returns_cupy_matching_cpu_2161(small_tif_bytes_2161, """HTTP URLs route through the CPU decode + GPU upload helper.""" import cupy - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu payload, arr_ref, _local = small_tif_bytes_2161 monkeypatch.setenv('XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS', '1') @@ -1277,7 +1277,7 @@ def test_http_url_returns_cupy_matching_cpu_2161(small_tif_bytes_2161, try: url = f'http://127.0.0.1:{port}/eager_2161.tif' da_cpu = open_geotiff(url) - da_gpu_direct = read_geotiff_gpu(url) + da_gpu_direct = _read_geotiff_gpu(url) da_gpu_open = open_geotiff(url, gpu=True) finally: httpd.shutdown() @@ -1298,7 +1298,7 @@ def test_memory_fsspec_uri_returns_cupy_matching_cpu_2161( fsspec = pytest.importorskip("fsspec") import cupy - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu payload, arr_ref, _local = small_tif_bytes_2161 fs = fsspec.filesystem("memory") @@ -1307,7 +1307,7 @@ def test_memory_fsspec_uri_returns_cupy_matching_cpu_2161( try: url = f"memory://{mem_path}" da_cpu = open_geotiff(url) - da_gpu_direct = read_geotiff_gpu(url) + da_gpu_direct = _read_geotiff_gpu(url) da_gpu_open = open_geotiff(url, gpu=True) finally: try: @@ -1328,7 +1328,7 @@ def test_unreachable_http_url_does_not_raise_filenotfound_2161(monkeypatch): """A bad URL no longer surfaces as a bare ``FileNotFoundError``.""" import socket - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu monkeypatch.setenv('XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS', '1') s = socket.socket() @@ -1338,7 +1338,7 @@ def test_unreachable_http_url_does_not_raise_filenotfound_2161(monkeypatch): url = f'http://127.0.0.1:{port}/nope_2161.tif' with pytest.raises(Exception) as excinfo: - read_geotiff_gpu(url) + _read_geotiff_gpu(url) err = excinfo.value assert not ( @@ -1469,10 +1469,10 @@ def test_chunked_gpu_declared_dtype_matches_computed_1909( def test_chunked_gpu_dtype_matches_cpu_dask_1909( uint16_no_sentinel_path_1909): """The dask+cupy declared dtype must match the dask+numpy path.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask from xrspatial.geotiff._backends.gpu import _read_geotiff_gpu_chunked_gds - cpu = read_geotiff_dask(uint16_no_sentinel_path_1909, chunks=4) + cpu = _read_geotiff_dask(uint16_no_sentinel_path_1909, chunks=4) ifd, geo_info, header = _parse_for_gds_1909(uint16_no_sentinel_path_1909) gpu = _read_geotiff_gpu_chunked_gds( uint16_no_sentinel_path_1909, ifd, geo_info, header, diff --git a/xrspatial/geotiff/tests/gpu/test_writer.py b/xrspatial/geotiff/tests/gpu/test_writer.py index bf9255e14..d2cf615ec 100644 --- a/xrspatial/geotiff/tests/gpu/test_writer.py +++ b/xrspatial/geotiff/tests/gpu/test_writer.py @@ -29,7 +29,7 @@ with the shared ``requires_gpu`` marker from ``_helpers/markers.py`` (aliased ``_gpu_only`` for brevity in this module). The CuPy-aware ``test_to_geotiff_gpu_fallback_1674.py`` tests never required a real -GPU (they monkeypatch ``write_geotiff_gpu``); they keep running on +GPU (they monkeypatch ``_write_geotiff_gpu``); they keep running on non-GPU hosts as before. """ from __future__ import annotations @@ -45,7 +45,7 @@ import xarray as xr from xrspatial.geotiff import (GeoTIFFFallbackWarning, _gpu_decode, open_geotiff, to_geotiff, - write_geotiff_gpu) + _write_geotiff_gpu) from xrspatial.geotiff._compression import JPEG2000_AVAILABLE, LERC_AVAILABLE, LZ4_AVAILABLE from xrspatial.geotiff._geotags import GeoTransform, _epsg_to_wkt from xrspatial.geotiff._header import parse_header, parse_ifd @@ -107,7 +107,7 @@ def test_crs_wkt_only_attr_round_trips(tmp_path): attrs={'crs_wkt': wkt}, ) out = str(tmp_path / 'crs_wkt_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) # The WKT should resolve back to EPSG 4326 on read. @@ -128,7 +128,7 @@ def test_image_description_round_trips_via_gpu_writer(tmp_path): attrs={'crs': 4326, 'image_description': 'gpu-attr-test-1563'}, ) out = str(tmp_path / 'desc_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) assert rd.attrs.get('image_description') == 'gpu-attr-test-1563' @@ -151,7 +151,7 @@ def test_extra_samples_single_band_writer_compat(tmp_path): attrs={'crs': 4326, 'extra_samples': [1]}, ) out = str(tmp_path / 'es_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) assert rd.attrs.get('crs') == 4326 @@ -175,7 +175,7 @@ def test_extra_samples_round_trips_multiband_via_gpu_writer(tmp_path): attrs={'crs': 4326}, ) out = str(tmp_path / 'es_multi_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) es = rd.attrs.get('extra_samples') @@ -206,7 +206,7 @@ def test_colormap_round_trips_via_gpu_writer(tmp_path): attrs={'crs': 4326, 'colormap': palette}, ) out = str(tmp_path / 'cmap_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) rd_cmap = rd.attrs.get('colormap') @@ -236,8 +236,8 @@ def test_extra_tags_custom_tag_round_trips_via_gpu_writer(tmp_path): out = str(tmp_path / 'extra_tags_1563.tif') # Rich-tag extra_tags is an experimental write surface. Opt in on # both write and read sides for the round-trip. - write_geotiff_gpu(da_gpu, out, compression='none', - allow_experimental_codecs=True) + _write_geotiff_gpu(da_gpu, out, compression='none', + allow_experimental_codecs=True) rd = open_geotiff(out) et = rd.attrs.get('extra_tags') or [] @@ -272,7 +272,7 @@ def test_resolution_tags_round_trip_via_gpu_writer(tmp_path): }, ) out = str(tmp_path / 'res_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) assert rd.attrs.get('x_resolution') == pytest.approx(300.0, rel=0.01), ( @@ -299,7 +299,7 @@ def test_gdal_metadata_round_trips_via_gpu_writer(tmp_path): 'CUSTOM_KEY': 'val_1563'}}, ) out = str(tmp_path / 'gdal_meta_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) meta = rd.attrs.get('gdal_metadata') or {} @@ -312,7 +312,7 @@ def test_gdal_metadata_round_trips_via_gpu_writer(tmp_path): @_gpu_only def test_transform_attr_round_trip_bit_stable(tmp_path): """Reading a file with a fractional pixel size, then writing it back - through ``write_geotiff_gpu`` must preserve ``attrs['transform']`` + through ``_write_geotiff_gpu`` must preserve ``attrs['transform']`` bit-for-bit (the CPU writer guarantees this; the GPU writer used to drop the attr and recompute from coords, which drifts). """ @@ -330,7 +330,7 @@ def test_transform_attr_round_trip_bit_stable(tmp_path): da_gpu = open_geotiff(src, gpu=True) out = str(tmp_path / 'frac_out_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') after = open_geotiff(out) assert after.attrs['transform'] == eager_in.attrs['transform'], ( @@ -353,7 +353,7 @@ def test_no_data_attr_still_round_trips_after_fix(tmp_path): attrs={'crs': 4326, 'nodata': -9999.0, 'raster_type': 'point'}, ) out = str(tmp_path / 'nodata_1563.tif') - write_geotiff_gpu(da_gpu, out, compression='none') + _write_geotiff_gpu(da_gpu, out, compression='none') rd = open_geotiff(out) assert rd.attrs.get('nodata') == -9999.0 @@ -369,7 +369,7 @@ def test_no_data_attr_still_round_trips_after_fix(tmp_path): @pytest.mark.parametrize("band_dim_name", ["band", "bands", "channel"]) def test_band_first_layout_written_correctly_via_write_geotiff_gpu( tmp_path, band_dim_name): - """Direct call to write_geotiff_gpu with a (band, y, x) CuPy + """Direct call to _write_geotiff_gpu with a (band, y, x) CuPy DataArray must produce the same file dimensions as the CPU writer would (height=y, width=x, samples=band). """ @@ -388,7 +388,7 @@ def test_band_first_layout_written_correctly_via_write_geotiff_gpu( ) out = str(tmp_path / f"band_first_1580_{band_dim_name}.tif") - write_geotiff_gpu(da, out, compression="none") + _write_geotiff_gpu(da, out, compression="none") rd = open_geotiff(out) assert rd.sizes["y"] == 16, ( @@ -455,7 +455,7 @@ def test_yxbands_layout_unchanged(tmp_path): ) out = str(tmp_path / "yxb_1580.tif") - write_geotiff_gpu(da, out, compression="none") + _write_geotiff_gpu(da, out, compression="none") rd = open_geotiff(out) assert rd.sizes == {"y": 8, "x": 12, "band": 2} @@ -490,7 +490,7 @@ def test_gpu_band_first_matches_cpu_byte_for_byte_on_pixel_values(tmp_path): cpu_path = str(tmp_path / "band_first_1580_cpu.tif") gpu_path = str(tmp_path / "band_first_1580_gpu.tif") to_geotiff(da_cpu, cpu_path, compression="none", gpu=False) - write_geotiff_gpu(da_gpu, gpu_path, compression="none") + _write_geotiff_gpu(da_gpu, gpu_path, compression="none") cpu_rd = open_geotiff(cpu_path).values gpu_rd = open_geotiff(gpu_path).values @@ -606,7 +606,7 @@ def test_write_geotiff_gpu_zstd_roundtrip(tmp_path): da, arr = _make_int_da() path = str(tmp_path / "zstd_roundtrip.tif") - write_geotiff_gpu(da, path, compression='zstd') + _write_geotiff_gpu(da, path, compression='zstd') out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -633,8 +633,8 @@ def test_write_geotiff_gpu_zstd_default_matches_explicit(tmp_path): default_path = str(tmp_path / "default.tif") explicit_path = str(tmp_path / "explicit_zstd.tif") - write_geotiff_gpu(da, default_path) - write_geotiff_gpu(da, explicit_path, compression='zstd') + _write_geotiff_gpu(da, default_path) + _write_geotiff_gpu(da, explicit_path, compression='zstd') assert _read_compression_tag(default_path) == _COMPRESSION_TAGS['zstd'] assert _read_compression_tag(explicit_path) == _COMPRESSION_TAGS['zstd'] @@ -661,7 +661,7 @@ def test_write_geotiff_gpu_jpeg_rgb_roundtrip(tmp_path): # The JPEG encode path is opt-in. The writer also emits a # GeoTIFFFallbackWarning, which is the documented contract. with pytest.warns(Warning): - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='jpeg', allow_internal_only_jpeg=True, ) @@ -689,7 +689,7 @@ def test_write_geotiff_gpu_jpeg_uint8_single_band_roundtrip(tmp_path): # Opt-in flag required; warning fires. with pytest.warns(Warning): - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='jpeg', allow_internal_only_jpeg=True, ) @@ -721,7 +721,7 @@ def test_write_geotiff_gpu_jpeg_uses_nvjpeg_when_available(tmp_path, # Opt-in flag required; warning fires. with pytest.warns(Warning): - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='jpeg', allow_internal_only_jpeg=True, ) @@ -748,7 +748,7 @@ def test_write_geotiff_gpu_compression_tag(tmp_path, compression): da, _ = _make_int_da() path = str(tmp_path / f"compression_tag_{compression}.tif") - write_geotiff_gpu(da, path, compression=compression) + _write_geotiff_gpu(da, path, compression=compression) assert _read_compression_tag(path) == _COMPRESSION_TAGS[compression] @@ -761,7 +761,7 @@ def test_write_geotiff_gpu_jpeg_compression_tag(tmp_path): # Opt-in flag required; warning fires. with pytest.warns(Warning): - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression='jpeg', allow_internal_only_jpeg=True, ) @@ -781,7 +781,7 @@ def test_write_geotiff_gpu_deflate_roundtrip(tmp_path): da, arr = _make_int_da() path = str(tmp_path / "deflate_plain.tif") - write_geotiff_gpu(da, path, compression='deflate') + _write_geotiff_gpu(da, path, compression='deflate') out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -799,7 +799,7 @@ def test_write_geotiff_gpu_none_roundtrip(tmp_path): da, arr = _make_int_da() path = str(tmp_path / "none_plain.tif") - write_geotiff_gpu(da, path, compression='none') + _write_geotiff_gpu(da, path, compression='none') out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -820,7 +820,7 @@ def test_write_geotiff_gpu_lossless_codecs_agree(tmp_path): for codec in ('none', 'deflate', 'zstd') } for codec, path in paths.items(): - write_geotiff_gpu(da, path, compression=codec) + _write_geotiff_gpu(da, path, compression=codec) reads = {codec: open_geotiff(path).values for codec, path in paths.items()} @@ -886,7 +886,7 @@ def test_write_geotiff_gpu_lzw_roundtrip(tmp_path): da, arr = _make_int_da_small() path = str(tmp_path / "lzw_roundtrip.tif") - write_geotiff_gpu(da, path, compression='lzw') + _write_geotiff_gpu(da, path, compression='lzw') out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -899,7 +899,7 @@ def test_write_geotiff_gpu_lzw_compression_tag(tmp_path): da, _ = _make_int_da_small() path = str(tmp_path / "lzw_tag.tif") - write_geotiff_gpu(da, path, compression='lzw') + _write_geotiff_gpu(da, path, compression='lzw') assert _read_compression_tag(path) == _COMPRESSION_TAGS['lzw'] @@ -910,7 +910,7 @@ def test_write_geotiff_gpu_packbits_roundtrip(tmp_path): da, arr = _make_int_da_small() path = str(tmp_path / "packbits_roundtrip.tif") - write_geotiff_gpu(da, path, compression='packbits') + _write_geotiff_gpu(da, path, compression='packbits') out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -923,7 +923,7 @@ def test_write_geotiff_gpu_packbits_compression_tag(tmp_path): da, _ = _make_int_da_small() path = str(tmp_path / "packbits_tag.tif") - write_geotiff_gpu(da, path, compression='packbits') + _write_geotiff_gpu(da, path, compression='packbits') assert _read_compression_tag(path) == _COMPRESSION_TAGS['packbits'] @@ -942,7 +942,7 @@ def test_write_geotiff_gpu_lz4_roundtrip(tmp_path): da, arr = _make_int_da_small() path = str(tmp_path / "lz4_roundtrip.tif") - write_geotiff_gpu(da, path, compression='lz4', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='lz4', allow_experimental_codecs=True) out = open_geotiff(path, allow_experimental_codecs=True) np.testing.assert_array_equal(out.values, arr) @@ -956,7 +956,7 @@ def test_write_geotiff_gpu_lz4_compression_tag(tmp_path): da, _ = _make_int_da_small() path = str(tmp_path / "lz4_tag.tif") - write_geotiff_gpu(da, path, compression='lz4', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='lz4', allow_experimental_codecs=True) assert _read_compression_tag(path) == _COMPRESSION_TAGS['lz4'] @@ -976,7 +976,7 @@ def test_write_geotiff_gpu_lerc_float_lossless_roundtrip(tmp_path): da, arr = _make_float_da_small(dtype=np.float32) path = str(tmp_path / "lerc_float.tif") - write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) out = open_geotiff(path, allow_experimental_codecs=True) np.testing.assert_array_equal(out.values, arr) @@ -996,7 +996,7 @@ def test_write_geotiff_gpu_lerc_int_roundtrip(tmp_path): da, arr = _make_int_da_small(dtype=np.uint16) path = str(tmp_path / "lerc_int.tif") - write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) out = open_geotiff(path, allow_experimental_codecs=True) np.testing.assert_array_equal(out.values, arr) @@ -1010,7 +1010,7 @@ def test_write_geotiff_gpu_lerc_compression_tag(tmp_path): da, _ = _make_float_da_small() path = str(tmp_path / "lerc_tag.tif") - write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='lerc', allow_experimental_codecs=True) assert _read_compression_tag(path) == _COMPRESSION_TAGS['lerc'] @@ -1033,7 +1033,7 @@ def test_write_geotiff_gpu_jpeg2000_uint8_lossless_roundtrip(tmp_path): da, arr = _make_int_da_small(dtype=np.uint8) path = str(tmp_path / "j2k_uint8.tif") - write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) out = open_geotiff(path, allow_experimental_codecs=True) np.testing.assert_array_equal(out.values, arr) @@ -1054,7 +1054,7 @@ def test_write_geotiff_gpu_jpeg2000_rgb_roundtrip(tmp_path): da, arr = _make_uint8_rgb_da_small() path = str(tmp_path / "j2k_rgb.tif") - write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) out = open_geotiff(path, allow_experimental_codecs=True) np.testing.assert_array_equal(out.values, arr) @@ -1075,8 +1075,8 @@ def test_write_geotiff_gpu_j2k_alias_matches_jpeg2000(tmp_path): j2k_path = str(tmp_path / "alias_j2k.tif") jpeg2k_path = str(tmp_path / "alias_jpeg2000.tif") - write_geotiff_gpu(da, j2k_path, compression='j2k', allow_experimental_codecs=True) - write_geotiff_gpu(da, jpeg2k_path, compression='jpeg2000', allow_experimental_codecs=True) + _write_geotiff_gpu(da, j2k_path, compression='j2k', allow_experimental_codecs=True) + _write_geotiff_gpu(da, jpeg2k_path, compression='jpeg2000', allow_experimental_codecs=True) assert _read_compression_tag(j2k_path) == _COMPRESSION_TAGS['j2k'] assert _read_compression_tag(jpeg2k_path) == _COMPRESSION_TAGS['jpeg2000'] @@ -1095,7 +1095,7 @@ def test_write_geotiff_gpu_jpeg2000_compression_tag(tmp_path): da, _ = _make_int_da_small(dtype=np.uint8) path = str(tmp_path / "j2k_tag.tif") - write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression='jpeg2000', allow_experimental_codecs=True) assert _read_compression_tag(path) == _COMPRESSION_TAGS['jpeg2000'] @@ -1127,7 +1127,7 @@ def test_write_geotiff_gpu_cpu_parity_lossless(tmp_path, compression): gpu_path = str(tmp_path / f"gpu_{compression}.tif") cpu_path = str(tmp_path / f"cpu_{compression}.tif") - write_geotiff_gpu(da, gpu_path, compression=compression) + _write_geotiff_gpu(da, gpu_path, compression=compression) # Build a CPU DataArray with the same input. to_geotiff(gpu=False) # writes through the matching CPU codec branch. @@ -1155,7 +1155,7 @@ def test_to_geotiff_gpu_true_dispatches_through_fallback_codec( succeed (rather than rejecting the codec at the dispatch layer). ``to_geotiff`` rejects ``compression='jpeg'`` outright and forwards - everything else to ``write_geotiff_gpu`` when ``gpu=True``. The + everything else to ``_write_geotiff_gpu`` when ``gpu=True``. The GPU writer documents the CPU-fallback codecs as accepted, so the auto-dispatch path must round-trip them too. A regression that added a fallback-codec rejection in the dispatch (a la jpeg) @@ -1198,7 +1198,7 @@ def test_gpu_writer_substitutes_nan_with_sentinel(tmp_path): da = xr.DataArray(cp.asarray(arr.copy()), dims=['y', 'x'], coords={'y': y, 'x': x}) p = str(tmp_path / "gpu_nan_sentinel.tif") - write_geotiff_gpu(da, p, crs=4326, nodata=-9999) + _write_geotiff_gpu(da, p, crs=4326, nodata=-9999) raw, _ = read_to_array(p) # The raw decoded bytes should contain the sentinel, not NaN. @@ -1225,7 +1225,7 @@ def test_gpu_and_cpu_writers_byte_equivalent_on_nan_input(tmp_path): p_cpu = str(tmp_path / "cpu.tif") p_gpu = str(tmp_path / "gpu.tif") to_geotiff(da_cpu, p_cpu, crs=4326, nodata=-9999) - write_geotiff_gpu(da_gpu, p_gpu, crs=4326, nodata=-9999) + _write_geotiff_gpu(da_gpu, p_gpu, crs=4326, nodata=-9999) raw_cpu, _ = read_to_array(p_cpu) raw_gpu, _ = read_to_array(p_gpu) @@ -1244,7 +1244,7 @@ def test_gpu_writer_preserves_caller_cupy_buffer(tmp_path): da = xr.DataArray(cp_arr, dims=['y', 'x'], coords={'y': y, 'x': x}) p = str(tmp_path / "gpu_preserve.tif") - write_geotiff_gpu(da, p, crs=4326, nodata=-9999) + _write_geotiff_gpu(da, p, crs=4326, nodata=-9999) after = cp.asnumpy(cp_arr) # NaNs must still be present at the original locations. @@ -1268,7 +1268,7 @@ def test_gpu_writer_no_rewrite_when_no_nans(tmp_path): da = xr.DataArray(cp.asarray(arr.copy()), dims=['y', 'x'], coords={'y': y, 'x': x}) p = str(tmp_path / "gpu_no_nans.tif") - write_geotiff_gpu(da, p, crs=4326, nodata=-9999) + _write_geotiff_gpu(da, p, crs=4326, nodata=-9999) raw, _ = read_to_array(p) assert np.array_equal(raw, arr) @@ -1284,7 +1284,7 @@ def test_gpu_writer_nan_nodata_skips_substitution(tmp_path): da = xr.DataArray(cp.asarray(arr.copy()), dims=['y', 'x'], coords={'y': y, 'x': x}) p = str(tmp_path / "gpu_nan_sentinel_nan.tif") - write_geotiff_gpu(da, p, crs=4326, nodata=float('nan')) + _write_geotiff_gpu(da, p, crs=4326, nodata=float('nan')) raw, _ = read_to_array(p) # NaN pixels remain NaN; finite pixels remain finite. @@ -1315,7 +1315,7 @@ def test_gpu_writer_external_reader_sees_correct_nodata_mask(tmp_path): # ship without ZSTD codec support and would fail this round-trip # for environment reasons unrelated to the nodata mask under test. to_geotiff(da_cpu, p_cpu, crs=4326, nodata=-9999, compression='deflate') - write_geotiff_gpu( + _write_geotiff_gpu( da_gpu, p_gpu, crs=4326, nodata=-9999, compression='deflate') with rasterio.open(p_cpu) as src: @@ -1343,7 +1343,7 @@ def test_gpu_writer_multiband_nan_substitution(tmp_path): coords={'y': y, 'x': x, 'band': np.arange(b)}) p = str(tmp_path / "gpu_mb.tif") - write_geotiff_gpu(da, p, crs=4326, nodata=-9999) + _write_geotiff_gpu(da, p, crs=4326, nodata=-9999) raw, _ = read_to_array(p) nan_locations = np.isnan(arr) @@ -1395,7 +1395,7 @@ def test_gpu_writer_overview_loop_uses_putmask_1948(): src = src_path.read_text() # The overview loop should call cupy.putmask, not current.copy() + indexed write. assert "cupy.putmask(" in src, ( - "write_geotiff_gpu overview loop should use cupy.putmask " + "_write_geotiff_gpu overview loop should use cupy.putmask " "for the in-place NaN->sentinel rewrite (issue #1948)." ) # Confirm the in-place pattern is the one inside the overview branch, @@ -1448,7 +1448,7 @@ def test_gpu_writer_cog_overview_sentinel_roundtrip_1948(): with tempfile.TemporaryDirectory() as td: path = os.path.join(td, "tmp_1948_cog.tif") - write_geotiff_gpu( + _write_geotiff_gpu( da, path, compression="deflate", tile_size=256, cog=True, overview_levels=[2, 4], ) @@ -1606,7 +1606,7 @@ def test_block_reduce_2d_gpu_mode_dtype_preserved(dtype): @_gpu_only def test_write_geotiff_gpu_cog_overview_resampling_mode(tmp_path): - """``write_geotiff_gpu(cog=True, overview_resampling='mode')`` writes + """``_write_geotiff_gpu(cog=True, overview_resampling='mode')`` writes a COG whose level-1 overview matches the closed-form 2x2 mode reduction. @@ -1622,7 +1622,7 @@ def test_write_geotiff_gpu_cog_overview_resampling_mode(tmp_path): coords={'y': np.arange(4.0, 0, -1), 'x': np.arange(4.0)}, ) p = str(tmp_path / 'cog_mode_gpu_1740.tif') - write_geotiff_gpu( + _write_geotiff_gpu( da, p, cog=True, compression='deflate', tiled=True, tile_size=16, overview_levels=[2], overview_resampling='mode', @@ -1642,7 +1642,7 @@ def test_write_geotiff_gpu_cog_overview_resampling_mode(tmp_path): def test_to_geotiff_gpu_cog_overview_resampling_mode(tmp_path): """``to_geotiff(gpu=True, cog=True, overview_resampling='mode')`` threads through to the GPU writer and produces the same overview as - the explicit ``write_geotiff_gpu`` call.""" + the explicit ``_write_geotiff_gpu`` call.""" import cupy arr = _mode_4x4_uint8() @@ -1736,7 +1736,7 @@ def test_write_geotiff_gpu_compression_level_in_range_accepted( p = str(tmp_path / f'level_in_{compression}_{in_range_level}_1740.tif') # Must not raise. - write_geotiff_gpu( + _write_geotiff_gpu( da, p, compression=compression, compression_level=in_range_level, tile_size=32, @@ -1771,7 +1771,7 @@ def test_write_geotiff_gpu_compression_level_out_of_range_accepted( p = str(tmp_path / f'level_oor_{compression}_{oor_level}_1740.tif') # Must not raise -- the GPU writer ignores the level. - write_geotiff_gpu( + _write_geotiff_gpu( da, p, compression=compression, compression_level=oor_level, tile_size=32, @@ -1786,7 +1786,7 @@ def test_write_geotiff_gpu_compression_level_out_of_range_accepted( @_gpu_only def test_to_geotiff_gpu_compression_level_out_of_range_accepted(tmp_path): """``to_geotiff(gpu=True, compression_level=X)`` threads the kwarg - through to ``write_geotiff_gpu`` and must not raise on an out-of-range + through to ``_write_geotiff_gpu`` and must not raise on an out-of-range value (the GPU writer ignores it). Without this test the dispatcher could be rewritten to validate @@ -1832,7 +1832,7 @@ def test_to_geotiff_cpu_compression_level_out_of_range_raises(tmp_path): # Section 8: to_geotiff(gpu=True) fallback contract # (was test_to_geotiff_gpu_fallback_1674.py) # -# These tests monkeypatch ``write_geotiff_gpu`` so they do not need a +# These tests monkeypatch ``_write_geotiff_gpu`` so they do not need a # real GPU; they exercise the CPU dispatcher's exception classification. # =========================================================================== @@ -1865,10 +1865,10 @@ def cpu_data(): def _patch_gpu_writer_to_raise(monkeypatch, exc): - """Replace ``write_geotiff_gpu`` (as resolved by ``to_geotiff``) with a + """Replace ``_write_geotiff_gpu`` (as resolved by ``to_geotiff``) with a stub that raises ``exc``. - ``to_geotiff`` calls ``write_geotiff_gpu`` directly inside its own + ``to_geotiff`` calls ``_write_geotiff_gpu`` directly inside its own defining module (``_writers.eager``), so the patch targets the module-level name there. Patching ``xrspatial.geotiff`` would only update the package re-export and would not intercept the @@ -1879,7 +1879,7 @@ def _patch_gpu_writer_to_raise(monkeypatch, exc): def _boom(*args, **kwargs): raise exc - monkeypatch.setattr(g, 'write_geotiff_gpu', _boom, raising=True) + monkeypatch.setattr(g, '_write_geotiff_gpu', _boom, raising=True) def test_runtime_error_without_gpu_signal_propagates( diff --git a/xrspatial/geotiff/tests/integration/test_dask_pipeline.py b/xrspatial/geotiff/tests/integration/test_dask_pipeline.py index b87959ecb..529f48566 100644 --- a/xrspatial/geotiff/tests/integration/test_dask_pipeline.py +++ b/xrspatial/geotiff/tests/integration/test_dask_pipeline.py @@ -10,7 +10,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import open_geotiff, read_geotiff_dask, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_geotiff_dask, to_geotiff from xrspatial.geotiff._writer import write # ---------------------------------------------------------- @@ -43,12 +43,12 @@ def test_chunk_smaller_than_tile(tmp_path, _arr_64x96_dask_chunk_tile_misalignme is off by one row or column, the computed value will differ from the source. """ - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path = tmp_path / "tiled_misalign_small.tif" _write_tiled_dask_chunk_tile_misalignment(path, _arr_64x96_dask_chunk_tile_misalignment, tile=16) # noqa: E501 - da_arr = read_geotiff_dask(str(path), chunks=11) + da_arr = _read_geotiff_dask(str(path), chunks=11) assert isinstance(da_arr.data, dask_array_dask_chunk_tile_misalignment.Array) # 11 < 16: every tile is dispersed across at least 2 chunks. assert da_arr.data.chunksize[:2] == (11, 11) @@ -63,12 +63,12 @@ def test_chunk_larger_than_tile_nonmultiple(tmp_path, _arr_64x96_dask_chunk_tile the nearest tile boundary, the chunk shape comes out wrong; if it rounds up, the values shift. """ - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path = tmp_path / "tiled_misalign_large.tif" _write_tiled_dask_chunk_tile_misalignment(path, _arr_64x96_dask_chunk_tile_misalignment, tile=16) # noqa: E501 - da_arr = read_geotiff_dask(str(path), chunks=23) + da_arr = _read_geotiff_dask(str(path), chunks=23) assert isinstance(da_arr.data, dask_array_dask_chunk_tile_misalignment.Array) assert da_arr.data.chunksize[:2] == (23, 23) np.testing.assert_array_equal(da_arr.compute().values, _arr_64x96_dask_chunk_tile_misalignment) @@ -81,7 +81,7 @@ def test_chunk_tuple_doubly_unaligned(tmp_path): final column chunk both crop, and neither chunk dimension is aligned with the tile grid. This is the corner-cell case. """ - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask rng = np.random.RandomState(0xDCED) arr = rng.randint(0, 256, size=(50, 70), dtype=np.uint8) @@ -89,7 +89,7 @@ def test_chunk_tuple_doubly_unaligned(tmp_path): path = tmp_path / "tiled_corner_misalign.tif" _write_tiled_dask_chunk_tile_misalignment(path, arr, tile=16) - da_arr = read_geotiff_dask(str(path), chunks=(17, 19)) + da_arr = _read_geotiff_dask(str(path), chunks=(17, 19)) assert da_arr.shape == (50, 70) # Last block in each axis is the trimmed remainder. block_h = da_arr.data.chunks[0] @@ -263,8 +263,8 @@ def test_default_max_pixels_guard_does_not_fire_up_front(tmp_path): _write_oversized_dask_max_pixels_default_guard(path, h=side, w=side) # The graph builds; the up-front guard is gone. Chunk-level decode # may still fail when a task actually runs, but that is a separate - # path. ``read_geotiff_dask(...)`` itself returns a DataArray. - da = read_geotiff_dask(str(path)) + # path. ``_read_geotiff_dask(...)`` itself returns a DataArray. + da = _read_geotiff_dask(str(path)) assert da.shape == (side, side) @@ -279,7 +279,7 @@ def test_explicit_max_pixels_enforced_per_chunk(tmp_path): str(path), arr, tile=(16, 16), photometric="minisblack", compression="none") # 32x32 chunk = 1024 pixels; cap is 100. Graph builds, compute fails. - da = read_geotiff_dask(str(path), chunks=32, max_pixels=100) + da = _read_geotiff_dask(str(path), chunks=32, max_pixels=100) with pytest.raises(ValueError, match="exceed the safety limit"): da.compute() @@ -291,7 +291,7 @@ def test_small_region_unaffected(tmp_path): tifffile_dask_max_pixels_default_guard.imwrite( str(path), arr, tile=(16, 16), photometric="minisblack", compression="none") - da = read_geotiff_dask(str(path), chunks=8) + da = _read_geotiff_dask(str(path), chunks=8) np.testing.assert_array_equal(da.compute().values, arr) # ---------------------------------------------------------- @@ -383,13 +383,13 @@ def wrapped_r2a(*args, **kwargs): captured.append(tracked) return tracked, meta - # ``read_geotiff_dask``'s per-chunk worker calls the alias + # ``_read_geotiff_dask``'s per-chunk worker calls the alias # ``_read_to_array`` bound in ``xrspatial.geotiff._backends.dask``. # Patch that binding; patching ``_reader.read_to_array`` would not # affect the already-imported alias. monkeypatch.setattr(gt, '_read_to_array', wrapped_r2a) - dk = read_geotiff_dask(path, chunks=4) + dk = _read_geotiff_dask(path, chunks=4) dk.compute() assert captured, "read_to_array was not invoked" @@ -406,7 +406,7 @@ def wrapped_r2a(*args, **kwargs): def test_caller_supplied_dtype_still_casts(float32_no_nodata_tif_dask_no_op_astype): """Explicit ``dtype=float64`` still triggers the cast.""" path, _ = float32_no_nodata_tif_dask_no_op_astype - dk = read_geotiff_dask(path, dtype=np.float64, chunks=4) + dk = _read_geotiff_dask(path, dtype=np.float64, chunks=4) assert dk.dtype == np.float64 out = dk.compute() assert out.dtype == np.float64 @@ -441,21 +441,21 @@ def _write_cog_with_overviews_dask_overview_level(path: str, data: np.ndarray) - def test_dask_overview_level_zero_matches_full_res(tmp_path): """``overview_level=0`` returns full resolution (the base IFD).""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask rng = np.random.RandomState(0xD0E) arr = rng.randint(0, 256, size=(128, 192), dtype=np.uint8) path = str(tmp_path / "cog_dask_ov.tif") _write_cog_with_overviews_dask_overview_level(path, arr) - da_arr = read_geotiff_dask(path, chunks=32, overview_level=0) + da_arr = _read_geotiff_dask(path, chunks=32, overview_level=0) assert da_arr.shape == arr.shape np.testing.assert_array_equal(da_arr.compute().values, arr) def test_dask_overview_level_one_returns_half_res(tmp_path): """``overview_level=1`` materialises the half-resolution overview.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_dask + from xrspatial.geotiff import open_geotiff, _read_geotiff_dask rng = np.random.RandomState(0xD0E) arr = rng.randint(0, 256, size=(128, 192), dtype=np.uint8) @@ -466,7 +466,7 @@ def test_dask_overview_level_one_returns_half_res(tmp_path): # pull the same bytes from the same IFD. eager = open_geotiff(path, overview_level=1) - da_arr = read_geotiff_dask(path, chunks=16, overview_level=1) + da_arr = _read_geotiff_dask(path, chunks=16, overview_level=1) assert da_arr.shape == eager.shape, ( f"dask returned {da_arr.shape} but eager returned {eager.shape} " "at overview_level=1" @@ -477,7 +477,7 @@ def test_dask_overview_level_one_returns_half_res(tmp_path): def test_dask_overview_level_two_returns_quarter_res(tmp_path): """``overview_level=2`` materialises the quarter-resolution overview.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_dask + from xrspatial.geotiff import open_geotiff, _read_geotiff_dask rng = np.random.RandomState(0xD0E) arr = rng.randint(0, 256, size=(128, 192), dtype=np.uint8) @@ -486,21 +486,21 @@ def test_dask_overview_level_two_returns_quarter_res(tmp_path): eager = open_geotiff(path, overview_level=2) - da_arr = read_geotiff_dask(path, chunks=8, overview_level=2) + da_arr = _read_geotiff_dask(path, chunks=8, overview_level=2) assert da_arr.shape == eager.shape np.testing.assert_array_equal(da_arr.compute().values, eager.values) def test_dask_overview_level_none_returns_full_res(tmp_path): """``overview_level=None`` keeps default behaviour: full resolution.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask rng = np.random.RandomState(0xD0E) arr = rng.randint(0, 256, size=(128, 192), dtype=np.uint8) path = str(tmp_path / "cog_dask_ov_none.tif") _write_cog_with_overviews_dask_overview_level(path, arr) - da_arr = read_geotiff_dask(path, chunks=32, overview_level=None) + da_arr = _read_geotiff_dask(path, chunks=32, overview_level=None) assert da_arr.shape == arr.shape np.testing.assert_array_equal(da_arr.compute().values, arr) @@ -551,8 +551,8 @@ def _make_data_dask_planar_multiband(bands: int, height: int, width: int, dtype) def test_dask_planar_multiband_matches_numpy( tmp_path, planar, tiled, bands, dtype ): - """``read_geotiff_dask`` returns ``(y, x, band)`` matching the source.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask`` returns ``(y, x, band)`` matching the source.""" + from xrspatial.geotiff import _read_geotiff_dask height, width = 96, 128 data = _make_data_dask_planar_multiband(bands, height, width, dtype) @@ -565,7 +565,7 @@ def test_dask_planar_multiband_matches_numpy( f"b{bands}_{np.dtype(dtype).name}.tif") _write_planar_tiff_dask_planar_multiband(path, data, planar=planar, tiled=tiled) - da_arr = read_geotiff_dask(path, chunks=32) + da_arr = _read_geotiff_dask(path, chunks=32) assert isinstance(da_arr.data, dask_array_dask_planar_multiband.Array), ( f"expected dask Array, got {type(da_arr.data).__name__}" @@ -582,7 +582,7 @@ def test_dask_planar_multiband_matches_numpy( def test_dask_planar_separate_chunks_tuple(tmp_path): """Tuple chunks ``(ch_h, ch_w)`` honoured; band axis stays single chunk.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask bands, height, width = 3, 80, 120 data = _make_data_dask_planar_multiband(bands, height, width, np.uint8) @@ -591,9 +591,9 @@ def test_dask_planar_separate_chunks_tuple(tmp_path): path = str(tmp_path / "dask_planar_chunktuple.tif") _write_planar_tiff_dask_planar_multiband(path, data, planar="separate", tiled=True) - da_arr = read_geotiff_dask(path, chunks=(40, 60)) + da_arr = _read_geotiff_dask(path, chunks=(40, 60)) - # ``read_geotiff_dask`` builds row-major chunks of (ch_h, ch_w, n_bands). + # ``_read_geotiff_dask`` builds row-major chunks of (ch_h, ch_w, n_bands). # With height=80, width=120, chunks=(40, 60) the expected layout is # 2 row blocks x 2 col blocks x 1 band block. assert da_arr.data.chunksize[:2] == (40, 60) diff --git a/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py b/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py index 6982b0947..514dbc5e3 100644 --- a/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py +++ b/xrspatial/geotiff/tests/integration/test_gpu_pipeline.py @@ -79,8 +79,8 @@ def test_open_geotiff_gpu_chunks_int_round_trip(tmp_path): def test_read_geotiff_gpu_chunks_tuple_round_trip(tmp_path): - """`read_geotiff_gpu(chunks=(rh, cw))` accepts tuple chunk specs.""" - from xrspatial.geotiff import open_geotiff, read_geotiff_gpu, to_geotiff + """`_read_geotiff_gpu(chunks=(rh, cw))` accepts tuple chunk specs.""" + from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu, to_geotiff rng = np.random.RandomState(11) arr = rng.randint(0, 60_000, (192, 256)).astype(np.uint16) @@ -89,7 +89,7 @@ def test_read_geotiff_gpu_chunks_tuple_round_trip(tmp_path): eager = np.asarray(open_geotiff(path).data) - da_arr = read_geotiff_gpu(path, chunks=(96, 128)) + da_arr = _read_geotiff_gpu(path, chunks=(96, 128)) computed = _assert_dask_cupy_dask_cupy_combined( da_arr, @@ -104,7 +104,7 @@ def test_read_geotiff_gpu_chunks_tuple_round_trip(tmp_path): def test_open_geotiff_gpu_chunks_multiband(tmp_path): """Combined backend round-trips a 3-band tiled raster. - Multi-band exercises the planar-config branch in `read_geotiff_gpu` + Multi-band exercises the planar-config branch in `_read_geotiff_gpu` that the chunks=None path also walks; without this, a planar-related refactor could leave the chunked path with a stale shape. """ diff --git a/xrspatial/geotiff/tests/integration/test_http_sources.py b/xrspatial/geotiff/tests/integration/test_http_sources.py index 2dedd062e..3e71fb462 100644 --- a/xrspatial/geotiff/tests/integration/test_http_sources.py +++ b/xrspatial/geotiff/tests/integration/test_http_sources.py @@ -24,8 +24,8 @@ from xrspatial.geotiff import UnsafeURLError from xrspatial.geotiff import _reader as _reader_mod from xrspatial.geotiff import _sources as _sources_mod -from xrspatial.geotiff import (open_geotiff, read_geotiff_dask, read_geotiff_gpu, read_vrt, - to_geotiff, write_geotiff_gpu, write_vrt) +from xrspatial.geotiff import (open_geotiff, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + to_geotiff, _write_geotiff_gpu, build_vrt) from xrspatial.geotiff._errors import RotatedTransformError from xrspatial.geotiff._header import parse_all_ifds, parse_header from xrspatial.geotiff._reader import (_FULL_IMAGE_BUDGET_HEADER_SLACK, INITIAL_HTTP_HEADER_BYTES, @@ -688,14 +688,14 @@ def test_read_cog_http_perf_with_mock_rtt(small_cog_bytes_http_cog_coalesce, mon # --------------------------------------------------------------------------- -# read_geotiff_dask: IFD parsing call count and correctness +# _read_geotiff_dask: IFD parsing call count and correctness # --------------------------------------------------------------------------- def test_dask_local_correctness(small_cog_bytes_http_cog_coalesce): """Dask read of a local COG must equal the eager read bit-for-bit.""" _, expected, path = small_cog_bytes_http_cog_coalesce eager = open_geotiff(path) - lazy = read_geotiff_dask(path, chunks=16).compute() + lazy = _read_geotiff_dask(path, chunks=16).compute() np.testing.assert_array_equal(np.asarray(eager), np.asarray(lazy)) np.testing.assert_array_equal(np.asarray(eager), expected) @@ -716,7 +716,7 @@ def _fake_http_source(url): # 16x16 chunks on 64x64 -> 16 chunks. Without P5 each chunk would # spawn its own _HTTPSource and fire its own (0, 16384) GET. - da_arr = read_geotiff_dask('http://mock/cog.tif', chunks=16).compute() + da_arr = _read_geotiff_dask('http://mock/cog.tif', chunks=16).compute() np.testing.assert_array_equal(np.asarray(da_arr), expected) # Count "header" GETs across every _HTTPSource instance the read @@ -1123,7 +1123,7 @@ def _fake_http_source(url, *_a, **_kw): # 256x256 image; 32x32 chunks -> 64 chunks. If header parsing happens # per chunk task we should see ~64 header GETs. The contract says # at most one. - da_arr = read_geotiff_dask('http://mock/cog.tif', chunks=32) + da_arr = _read_geotiff_dask('http://mock/cog.tif', chunks=32) n_chunks = da_arr.data.npartitions assert n_chunks >= 16, ( f"expected >=16 chunks to make the count assertion meaningful, " @@ -1166,7 +1166,7 @@ def _fake(url, *_a, **_kw): return s monkeypatch.setattr(_reader_mod, '_HTTPSource', _fake) - out = read_geotiff_dask('http://mock/cog.tif', chunks=chunks).compute() + out = _read_geotiff_dask('http://mock/cog.tif', chunks=chunks).compute() np.testing.assert_array_equal(np.asarray(out), expected) return sum( 1 @@ -1634,7 +1634,7 @@ def test_http_dask_rotated_default_raises(tmp_path, monkeypatch): def test_http_dask_rotated_allow_rotated_reads(tmp_path, monkeypatch): """``allow_rotated=True`` over HTTP+dask reads the pixel grid. - A regression where ``read_geotiff_dask`` did not forward the kwarg + A regression where ``_read_geotiff_dask`` did not forward the kwarg to ``_parse_cog_http_meta`` would raise ``NotImplementedError``. """ monkeypatch.setenv('XRSPATIAL_GEOTIFF_ALLOW_PRIVATE_HOSTS', '1') @@ -5969,13 +5969,13 @@ def _fake_cloud_source(url, **kw): return seen, _Sentinel def test_uppercase_url_constructs_http_source(self, monkeypatch): - from xrspatial.geotiff._backends.dask import read_geotiff_dask + from xrspatial.geotiff._backends.dask import _read_geotiff_dask seen, _Sentinel = self._stub_dask_http_path(monkeypatch) url = f'HTTP://example.com/x_dask_{_ISSUE_2323}.tif' with pytest.raises(_Sentinel, match="_HTTPSource"): - read_geotiff_dask(url) + _read_geotiff_dask(url) assert seen['http'] == 1, ( "uppercase URL did not reach _HTTPSource; " @@ -5986,13 +5986,13 @@ def test_uppercase_url_constructs_http_source(self, monkeypatch): assert seen['url'] == url def test_lowercase_url_still_constructs_http_source(self, monkeypatch): - from xrspatial.geotiff._backends.dask import read_geotiff_dask + from xrspatial.geotiff._backends.dask import _read_geotiff_dask seen, _Sentinel = self._stub_dask_http_path(monkeypatch) url = f'http://example.com/x_dask_{_ISSUE_2323}.tif' with pytest.raises(_Sentinel, match="_HTTPSource"): - read_geotiff_dask(url) + _read_geotiff_dask(url) assert seen['http'] == 1 assert seen['cloud'] == 0 @@ -6001,13 +6001,13 @@ def test_uppercase_s3_url_still_constructs_cloud_source( self, monkeypatch): # Counter-check: a real fsspec URI (uppercase scheme too) must # still go to the cloud branch. - from xrspatial.geotiff._backends.dask import read_geotiff_dask + from xrspatial.geotiff._backends.dask import _read_geotiff_dask seen, _Sentinel = self._stub_dask_http_path(monkeypatch) url = f'S3://bucket/x_dask_{_ISSUE_2323}.tif' with pytest.raises(_Sentinel, match="_CloudSource"): - read_geotiff_dask(url) + _read_geotiff_dask(url) assert seen['cloud'] == 1 assert seen['http'] == 0 @@ -6043,7 +6043,7 @@ def _build_vrt_2026_05_15(tmp_path): """Build a 1-source VRT mosaic referencing a small local GeoTIFF.""" src = _build_local_tif_2026_05_15(tmp_path, name='vrt_src.tif') vrt = str(tmp_path / 'mosaic.vrt') - write_vrt(vrt, [src]) + build_vrt(vrt, [src]) return vrt, src @@ -6112,7 +6112,7 @@ def test_dispatcher_dask_path_rejects_max_cloud_bytes_2026_05_15(tmp_path): """``chunks=N`` with ``max_cloud_bytes=...`` raises ValueError. The kwarg is only consumed on the eager non-VRT path; the dask - branch (``read_geotiff_dask``) never references it. + branch (``_read_geotiff_dask``) never references it. """ path = _build_local_tif_2026_05_15(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): @@ -6122,7 +6122,7 @@ def test_dispatcher_dask_path_rejects_max_cloud_bytes_2026_05_15(tmp_path): def test_dispatcher_vrt_path_rejects_max_cloud_bytes_2026_05_15(tmp_path): """``.vrt`` source with ``max_cloud_bytes=...`` raises ValueError. - The kwarg is only consumed on the eager non-VRT path; ``read_vrt`` + The kwarg is only consumed on the eager non-VRT path; ``_read_vrt`` never references it. """ vrt, _src = _build_vrt_2026_05_15(tmp_path) @@ -6224,12 +6224,12 @@ def test_explicit_none_max_cloud_bytes_rejected_on_vrt_path_2026_05_15( # ---------------------------------------------------------- _PUBLIC_ENTRY_POINTS_2106 = ( open_geotiff, - read_geotiff_gpu, - read_geotiff_dask, - read_vrt, + _read_geotiff_gpu, + _read_geotiff_dask, + _read_vrt, to_geotiff, - write_geotiff_gpu, - write_vrt, + _write_geotiff_gpu, + build_vrt, ) diff --git a/xrspatial/geotiff/tests/parity/test_api_consolidation.py b/xrspatial/geotiff/tests/parity/test_api_consolidation.py new file mode 100644 index 000000000..686f1a584 --- /dev/null +++ b/xrspatial/geotiff/tests/parity/test_api_consolidation.py @@ -0,0 +1,120 @@ +"""Public API consolidation contract (issue #2960). + +The geotiff read/write surface is ``open_geotiff`` / ``to_geotiff`` plus +the ``build_vrt`` mosaic helper. The four data backends +(``_read_geotiff_dask``, ``_read_geotiff_gpu``, ``_read_vrt``, +``_write_geotiff_gpu``) are private; the dispatchers route to them from +the ``gpu=`` / ``chunks=`` / ``.vrt`` kwargs. ``build_vrt`` stays public +because it indexes files that already exist and has no DataArray to write. + +These tests pin the new surface and confirm the dispatchers still reach +each backend. +""" +import numpy as np +import pytest + +import xrspatial.geotiff as g +from xrspatial.geotiff import build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff._backends.dask import _read_geotiff_dask +from xrspatial.geotiff._backends.gpu import _read_geotiff_gpu +from xrspatial.geotiff._backends.vrt import _read_vrt +from xrspatial.geotiff._geotags import GeoTransform +from xrspatial.geotiff._writer import write +from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu + +from .._helpers.markers import requires_gpu + +_OLD_PUBLIC_NAMES = ( + "read_geotiff_dask", + "read_geotiff_gpu", + "read_vrt", + "write_geotiff_gpu", + "write_vrt", +) + + +def test_public_read_write_surface_is_consolidated(): + """The only lowercase ``__all__`` entries are the three public funcs.""" + fns = {name for name in g.__all__ if name[0].islower()} + assert fns == {"open_geotiff", "to_geotiff", "build_vrt"} + + +@pytest.mark.parametrize("name", _OLD_PUBLIC_NAMES) +def test_old_backend_names_removed_from_public_namespace(name): + """The five backend-named functions no longer exist on the package.""" + assert name not in g.__all__ + assert not hasattr(g, name) + + +def test_private_backends_remain_importable(): + """Made private, not deleted -- direct callers can still reach them.""" + for fn in (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + _write_geotiff_gpu): + assert callable(fn) + + +def _two_tile_vrt(tmp_path): + """Two side-by-side 4x4 tiles indexed by a VRT mosaic; returns the path.""" + left = np.arange(16, dtype=np.float32).reshape(4, 4) + right = np.arange(16, 32, dtype=np.float32).reshape(4, 4) + gt_left = GeoTransform(origin_x=0.0, origin_y=4.0, + pixel_width=1.0, pixel_height=-1.0) + gt_right = GeoTransform(origin_x=4.0, origin_y=4.0, + pixel_width=1.0, pixel_height=-1.0) + lpath = str(tmp_path / "left.tif") + rpath = str(tmp_path / "right.tif") + write(left, lpath, geo_transform=gt_left, compression="none", tiled=False) + write(right, rpath, geo_transform=gt_right, compression="none", tiled=False) + vrt_path = str(tmp_path / "mosaic.vrt") + build_vrt(vrt_path, [lpath, rpath]) + return vrt_path + + +def test_build_vrt_roundtrips_through_open_geotiff(tmp_path): + """build_vrt is the public mosaic builder; open_geotiff reads it back.""" + vrt_path = _two_tile_vrt(tmp_path) + mosaic = open_geotiff(vrt_path) + assert mosaic.shape == (4, 8) + + +def test_open_geotiff_vrt_matches_direct_backend(tmp_path): + """``.vrt`` source dispatches to the private VRT reader.""" + vrt_path = _two_tile_vrt(tmp_path) + np.testing.assert_array_equal( + open_geotiff(vrt_path).values, _read_vrt(vrt_path).values) + + +def test_open_geotiff_chunks_matches_direct_dask_backend(tmp_path): + """``chunks=`` dispatches to the private dask reader.""" + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + path = str(tmp_path / "src.tif") + to_geotiff(arr, path, compression="deflate") + via_dispatch = open_geotiff(path, chunks=4) + direct = _read_geotiff_dask(path, chunks=4) + assert via_dispatch.chunks is not None + np.testing.assert_array_equal( + via_dispatch.data.compute(), direct.data.compute()) + + +@requires_gpu +def test_open_geotiff_gpu_matches_direct_backend(tmp_path): + """``gpu=True`` dispatches to the private GPU reader.""" + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + path = str(tmp_path / "src.tif") + to_geotiff(arr, path, compression="deflate") + via_dispatch = open_geotiff(path, gpu=True) + direct = _read_geotiff_gpu(path) + np.testing.assert_array_equal( + via_dispatch.data.get(), direct.data.get()) + + +@requires_gpu +def test_to_geotiff_gpu_matches_direct_backend(tmp_path): + """``gpu=True`` on write dispatches to the private GPU writer.""" + arr = np.arange(64, dtype=np.float32).reshape(8, 8) + via_path = str(tmp_path / "via.tif") + direct_path = str(tmp_path / "direct.tif") + to_geotiff(arr, via_path, gpu=True, compression="deflate") + _write_geotiff_gpu(arr, direct_path, compression="deflate") + np.testing.assert_array_equal( + open_geotiff(via_path).values, open_geotiff(direct_path).values) diff --git a/xrspatial/geotiff/tests/parity/test_backend_matrix.py b/xrspatial/geotiff/tests/parity/test_backend_matrix.py index 546065b7f..3ba98db4d 100644 --- a/xrspatial/geotiff/tests/parity/test_backend_matrix.py +++ b/xrspatial/geotiff/tests/parity/test_backend_matrix.py @@ -71,7 +71,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import open_geotiff, read_vrt, to_geotiff, write_vrt +from xrspatial.geotiff import open_geotiff, _read_vrt, to_geotiff, build_vrt from xrspatial.geotiff._attrs import _finalize_eager_read, _finalize_lazy_read_attrs from xrspatial.geotiff._errors import RotatedTransformError, UnparseableCRSError @@ -407,7 +407,7 @@ def _build_vrt_mosaic(dir_path: Path, target: Path) -> Path: p = dir_path / f"{target.stem}_tile_{c}.tif" to_geotiff(da, str(p), compression="none", tiled=False) tile_paths.append(str(p)) - write_vrt(str(target), tile_paths, relative=False, crs=4326) + build_vrt(str(target), tile_paths, relative=False, crs=4326) return target @@ -715,8 +715,8 @@ def assert_parity( Run against an already-read DataArray rather than re-opening here so the same helper applies to both ``open_geotiff(path, **kwargs)`` and - the explicit ``read_geotiff_dask`` / ``read_geotiff_gpu`` / - ``read_vrt`` entry points. ``ref`` is the eager-numpy read of the + the explicit ``_read_geotiff_dask`` / ``_read_geotiff_gpu`` / + ``_read_vrt`` entry points. ``ref`` is the eager-numpy read of the same fixture, used as the reference for the pixel array, coord values, dims, and transform tuple. @@ -1178,7 +1178,7 @@ def _fp_read_vrt_eager(path: pathlib.Path, fixture_id: str) -> xr.DataArray: shutil.copy2(path, local_src) vrt_path = cache_dir / f"{fixture_id}.vrt" if not vrt_path.exists(): - write_vrt(str(vrt_path), [str(local_src)]) + build_vrt(str(vrt_path), [str(local_src)]) return open_geotiff(str(vrt_path), **_FP_OPTIN) @@ -1806,7 +1806,7 @@ def _ap_open_dask_gpu(path): def _ap_open_vrt(path, meta): - """Wrap the TIFF in a single-source VRT and read via ``read_vrt``. + """Wrap the TIFF in a single-source VRT and read via ``_read_vrt``. GDAL GeoTransform XML expects the upper-left CORNER as origin while ``_ap_coord_array`` uses center-based coords, so the corner is @@ -1848,7 +1848,7 @@ def _ap_open_vrt(path, meta): ) with open(vrt_path, 'w') as f: f.write(xml) - return read_vrt(vrt_path) + return _read_vrt(vrt_path) _AP_BACKENDS = ( diff --git a/xrspatial/geotiff/tests/parity/test_finalization.py b/xrspatial/geotiff/tests/parity/test_finalization.py index 853206e7e..419a854c9 100644 --- a/xrspatial/geotiff/tests/parity/test_finalization.py +++ b/xrspatial/geotiff/tests/parity/test_finalization.py @@ -10,7 +10,7 @@ entry point so ``overview_level``, ``max_cloud_bytes``, ``missing_sources``, ``band_nodata``, ``on_gpu_failure``, and the file-like-source guard reject identically across ``open_geotiff`` / - ``read_geotiff_dask`` / ``read_geotiff_gpu`` / ``read_vrt``. + ``_read_geotiff_dask`` / ``_read_geotiff_gpu`` / ``_read_vrt``. Section 2 -- Eager finalization parity ``_finalize_eager_read`` stamps the same nodata / georef attrs on the @@ -20,8 +20,8 @@ Section 3 -- Lazy finalization parity ``_finalize_lazy_read_attrs`` stamps the same attrs on the two dask - backends (``read_geotiff_dask`` and the dask branch of - ``read_geotiff_gpu``). Covers the five georef states plus the + backends (``_read_geotiff_dask`` and the dask branch of + ``_read_geotiff_gpu``). Covers the five georef states plus the ``nodata_pixels_present`` / ``nodata_dtype_cast`` lazy contract. GPU and dask+GPU rows skip when cupy + CUDA are absent via the shared @@ -35,8 +35,8 @@ import pytest import xarray as xr -from xrspatial.geotiff import (open_geotiff, read_geotiff_dask, read_geotiff_gpu, read_vrt, - to_geotiff, write_vrt) +from xrspatial.geotiff import (open_geotiff, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + to_geotiff, build_vrt) from xrspatial.geotiff._attrs import (GEOREF_STATUS_CRS_ONLY, GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_ROTATED_DROPPED, GEOREF_STATUS_TRANSFORM_ONLY) @@ -77,7 +77,7 @@ def _build_vrt(tmp_path): """Build a 1-source VRT mosaic referencing a small local GeoTIFF.""" src = _build_local_tif(tmp_path, name='vrt_src_2175.tif') vrt = str(tmp_path / 'mosaic_2175.vrt') - write_vrt(vrt, [src]) + build_vrt(vrt, [src]) return vrt, src @@ -107,57 +107,57 @@ def test_open_geotiff_overview_level_float(tmp_path): def test_dask_overview_level_bool(tmp_path, value): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="bool"): - read_geotiff_dask(path, overview_level=value) + _read_geotiff_dask(path, overview_level=value) def test_dask_overview_level_str(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="str"): - read_geotiff_dask(path, overview_level="0") + _read_geotiff_dask(path, overview_level="0") def test_dask_overview_level_float(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="float"): - read_geotiff_dask(path, overview_level=1.0) + _read_geotiff_dask(path, overview_level=1.0) @pytest.mark.parametrize("value", [True, False]) def test_gpu_overview_level_bool(tmp_path, value): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu(path, overview_level=value) + _read_geotiff_gpu(path, overview_level=value) def test_gpu_overview_level_str(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="str"): - read_geotiff_gpu(path, overview_level="0") + _read_geotiff_gpu(path, overview_level="0") def test_gpu_overview_level_float(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(TypeError, match="float"): - read_geotiff_gpu(path, overview_level=1.0) + _read_geotiff_gpu(path, overview_level=1.0) @pytest.mark.parametrize("value", [True, False]) def test_vrt_overview_level_bool(tmp_path, value): vrt, _src = _build_vrt(tmp_path) with pytest.raises(TypeError, match="bool"): - read_vrt(vrt, overview_level=value) + _read_vrt(vrt, overview_level=value) def test_vrt_overview_level_str(tmp_path): vrt, _src = _build_vrt(tmp_path) with pytest.raises(TypeError, match="str"): - read_vrt(vrt, overview_level="0") + _read_vrt(vrt, overview_level="0") def test_vrt_overview_level_float(tmp_path): vrt, _src = _build_vrt(tmp_path) with pytest.raises(TypeError, match="float"): - read_vrt(vrt, overview_level=1.0) + _read_vrt(vrt, overview_level=1.0) # --- max_cloud_bytes incompatibility through every applicable backend --- @@ -184,19 +184,19 @@ def test_open_geotiff_vrt_rejects_max_cloud_bytes(tmp_path): def test_dask_rejects_max_cloud_bytes(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_geotiff_dask(path, max_cloud_bytes=8) + _read_geotiff_dask(path, max_cloud_bytes=8) def test_gpu_rejects_max_cloud_bytes(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_geotiff_gpu(path, max_cloud_bytes=8) + _read_geotiff_gpu(path, max_cloud_bytes=8) def test_vrt_rejects_max_cloud_bytes(tmp_path): vrt, _src = _build_vrt(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_vrt(vrt, max_cloud_bytes=8) + _read_vrt(vrt, max_cloud_bytes=8) def test_explicit_none_max_cloud_bytes_rejected_on_dask_direct(tmp_path): @@ -207,19 +207,19 @@ def test_explicit_none_max_cloud_bytes_rejected_on_dask_direct(tmp_path): """ path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_geotiff_dask(path, max_cloud_bytes=None) + _read_geotiff_dask(path, max_cloud_bytes=None) def test_explicit_none_max_cloud_bytes_rejected_on_gpu_direct(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_geotiff_gpu(path, max_cloud_bytes=None) + _read_geotiff_gpu(path, max_cloud_bytes=None) def test_explicit_none_max_cloud_bytes_rejected_on_vrt_direct(tmp_path): vrt, _src = _build_vrt(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): - read_vrt(vrt, max_cloud_bytes=None) + _read_vrt(vrt, max_cloud_bytes=None) # --- missing_sources on non-VRT sources --- @@ -234,13 +234,13 @@ def test_open_geotiff_rejects_missing_sources_on_tif(tmp_path): def test_dask_rejects_missing_sources_on_tif(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"missing_sources only applies"): - read_geotiff_dask(path, missing_sources='raise') + _read_geotiff_dask(path, missing_sources='raise') def test_gpu_rejects_missing_sources_on_tif(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"missing_sources only applies"): - read_geotiff_gpu(path, missing_sources='raise') + _read_geotiff_gpu(path, missing_sources='raise') # --- band_nodata on non-VRT sources --- @@ -255,13 +255,13 @@ def test_open_geotiff_rejects_band_nodata_on_tif(tmp_path): def test_dask_rejects_band_nodata_on_tif(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"band_nodata only applies"): - read_geotiff_dask(path, band_nodata='first') + _read_geotiff_dask(path, band_nodata='first') def test_gpu_rejects_band_nodata_on_tif(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"band_nodata only applies"): - read_geotiff_gpu(path, band_nodata='first') + _read_geotiff_gpu(path, band_nodata='first') # --- on_gpu_failure when GPU is disabled --- @@ -276,13 +276,13 @@ def test_open_geotiff_rejects_on_gpu_failure_when_gpu_false(tmp_path): def test_dask_rejects_on_gpu_failure(tmp_path): path = _build_local_tif(tmp_path) with pytest.raises(ValueError, match=r"on_gpu_failure only applies"): - read_geotiff_dask(path, on_gpu_failure='strict') + _read_geotiff_dask(path, on_gpu_failure='strict') def test_vrt_rejects_on_gpu_failure(tmp_path): vrt, _src = _build_vrt(tmp_path) with pytest.raises(ValueError, match=r"on_gpu_failure only applies"): - read_vrt(vrt, on_gpu_failure='strict') + _read_vrt(vrt, on_gpu_failure='strict') # --- File-like sources reject gpu=True / chunks=... --- @@ -315,7 +315,7 @@ def test_dask_rejects_file_like(tmp_path): with pytest.raises( ValueError, match=r"chunks=\.\.\. \(dask\) is not supported for file-like"): - read_geotiff_dask(buf) + _read_geotiff_dask(buf) def test_gpu_rejects_file_like(tmp_path): @@ -325,7 +325,7 @@ def test_gpu_rejects_file_like(tmp_path): with pytest.raises( ValueError, match=r"gpu=True is not supported for file-like"): - read_geotiff_gpu(buf) + _read_geotiff_gpu(buf) # --- Path-object sources survive the helper's file-like guard --- @@ -341,14 +341,14 @@ def test_open_geotiff_accepts_path_object(tmp_path): def test_dask_accepts_path_object(tmp_path): from pathlib import Path path = _build_local_tif(tmp_path) - out = read_geotiff_dask(Path(path), chunks=4) + out = _read_geotiff_dask(Path(path), chunks=4) assert out.shape == (8, 8) def test_vrt_accepts_path_object(tmp_path): from pathlib import Path vrt, _src = _build_vrt(tmp_path) - out = read_vrt(Path(vrt)) + out = _read_vrt(Path(vrt)) assert out.shape == (8, 8) @@ -356,7 +356,7 @@ def test_vrt_accepts_path_object(tmp_path): def test_gpu_accepts_path_object(tmp_path): from pathlib import Path path = _build_local_tif(tmp_path) - out = read_geotiff_gpu(Path(path)) + out = _read_geotiff_gpu(Path(path)) assert out.shape == (8, 8) @@ -374,7 +374,7 @@ def test_gpu_path_object_does_not_raise_file_like_error(tmp_path): # GPU reason. The one thing it must NOT raise is the file-like # ValueError introduced by the validator misclassifying Path. try: - read_geotiff_gpu(Path(path)) + _read_geotiff_gpu(Path(path)) except ValueError as e: assert "file-like" not in str(e), ( f"validator misclassified Path as file-like: {e}" @@ -397,13 +397,13 @@ def test_open_geotiff_defaults_round_trip(tmp_path): def test_dask_defaults_round_trip(tmp_path): path = _build_local_tif(tmp_path) - out = read_geotiff_dask(path) + out = _read_geotiff_dask(path) assert out.shape == (8, 8) def test_vrt_defaults_round_trip(tmp_path): vrt, _src = _build_vrt(tmp_path) - out = read_vrt(vrt) + out = _read_vrt(vrt) assert out.shape == (8, 8) @@ -427,7 +427,7 @@ def test_max_cloud_bytes_message_parity(tmp_path): path = _build_local_tif(tmp_path) vrt, _ = _build_vrt(tmp_path) open_dask = _get_error(open_geotiff, path, chunks=4, max_cloud_bytes=8) - direct_dask = _get_error(read_geotiff_dask, path, max_cloud_bytes=8) + direct_dask = _get_error(_read_geotiff_dask, path, max_cloud_bytes=8) # Both raise ValueError with the same dask-incompatibility message. assert open_dask[0] == "ValueError" assert direct_dask[0] == "ValueError" @@ -436,7 +436,7 @@ def test_max_cloud_bytes_message_parity(tmp_path): assert "dask" in msg open_gpu = _get_error(open_geotiff, path, gpu=True, max_cloud_bytes=8) - direct_gpu = _get_error(read_geotiff_gpu, path, max_cloud_bytes=8) + direct_gpu = _get_error(_read_geotiff_gpu, path, max_cloud_bytes=8) assert open_gpu[0] == "ValueError" assert direct_gpu[0] == "ValueError" for _, msg in (open_gpu, direct_gpu): @@ -444,7 +444,7 @@ def test_max_cloud_bytes_message_parity(tmp_path): assert "gpu" in msg.lower() open_vrt = _get_error(open_geotiff, vrt, max_cloud_bytes=8) - direct_vrt = _get_error(read_vrt, vrt, max_cloud_bytes=8) + direct_vrt = _get_error(_read_vrt, vrt, max_cloud_bytes=8) assert open_vrt[0] == "ValueError" assert direct_vrt[0] == "ValueError" for _, msg in (open_vrt, direct_vrt): @@ -456,8 +456,8 @@ def test_band_nodata_message_parity(tmp_path): path = _build_local_tif(tmp_path) results = [ _get_error(open_geotiff, path, band_nodata='first'), - _get_error(read_geotiff_dask, path, band_nodata='first'), - _get_error(read_geotiff_gpu, path, band_nodata='first'), + _get_error(_read_geotiff_dask, path, band_nodata='first'), + _get_error(_read_geotiff_gpu, path, band_nodata='first'), ] for kind, msg in results: assert kind == "ValueError" @@ -468,8 +468,8 @@ def test_missing_sources_message_parity(tmp_path): path = _build_local_tif(tmp_path) results = [ _get_error(open_geotiff, path, missing_sources='raise'), - _get_error(read_geotiff_dask, path, missing_sources='raise'), - _get_error(read_geotiff_gpu, path, missing_sources='raise'), + _get_error(_read_geotiff_dask, path, missing_sources='raise'), + _get_error(_read_geotiff_gpu, path, missing_sources='raise'), ] for kind, msg in results: assert kind == "ValueError" @@ -481,8 +481,8 @@ def test_on_gpu_failure_message_parity(tmp_path): vrt, _ = _build_vrt(tmp_path) results = [ _get_error(open_geotiff, path, on_gpu_failure='strict'), - _get_error(read_geotiff_dask, path, on_gpu_failure='strict'), - _get_error(read_vrt, vrt, on_gpu_failure='strict'), + _get_error(_read_geotiff_dask, path, on_gpu_failure='strict'), + _get_error(_read_vrt, vrt, on_gpu_failure='strict'), ] for kind, msg in results: assert kind == "ValueError" @@ -494,9 +494,9 @@ def test_overview_level_message_parity(tmp_path): vrt, _ = _build_vrt(tmp_path) results = [ _get_error(open_geotiff, path, overview_level="bad"), - _get_error(read_geotiff_dask, path, overview_level="bad"), - _get_error(read_geotiff_gpu, path, overview_level="bad"), - _get_error(read_vrt, vrt, overview_level="bad"), + _get_error(_read_geotiff_dask, path, overview_level="bad"), + _get_error(_read_geotiff_gpu, path, overview_level="bad"), + _get_error(_read_vrt, vrt, overview_level="bad"), ] for kind, msg in results: assert kind == "TypeError" @@ -815,19 +815,19 @@ def test_multiband_stripped_parity(tmp_path): # =========================================================================== # # ``_finalize_lazy_read_attrs`` centralises the validate-then-populate-then- -# stamp logic shared by ``read_geotiff_dask`` (CPU+dask) and the dask branch -# of ``read_geotiff_gpu`` (GPU+dask). Each test opens the same fixture +# stamp logic shared by ``_read_geotiff_dask`` (CPU+dask) and the dask branch +# of ``_read_geotiff_gpu`` (GPU+dask). Each test opens the same fixture # through both backends and compares the attrs. tifffile = pytest.importorskip("tifffile") def _open_cpu_dask(path, **kwargs): - return read_geotiff_dask(path, chunks=2, **kwargs) + return _read_geotiff_dask(path, chunks=2, **kwargs) def _open_gpu_dask(path, **kwargs): - return read_geotiff_gpu(path, chunks=2, **kwargs) + return _read_geotiff_gpu(path, chunks=2, **kwargs) _BACKENDS = [ diff --git a/xrspatial/geotiff/tests/parity/test_pixel_equality.py b/xrspatial/geotiff/tests/parity/test_pixel_equality.py index 4967280b8..40d2653c9 100644 --- a/xrspatial/geotiff/tests/parity/test_pixel_equality.py +++ b/xrspatial/geotiff/tests/parity/test_pixel_equality.py @@ -6,8 +6,8 @@ * Pixel-byte parity across (numpy / dask+numpy / cupy / dask+cupy) on a representative dtype + compression + layout matrix, plus VRT, COG, BigTIFF, and MinIsWhite fixtures. -* Cross-entry-point parity: ``read_geotiff_dask``, ``read_geotiff_gpu``, - and ``read_vrt`` agree with ``open_geotiff`` for the same source. +* Cross-entry-point parity: ``_read_geotiff_dask``, ``_read_geotiff_gpu``, + and ``_read_vrt`` agree with ``open_geotiff`` for the same source. * Kwarg threading through the dispatcher: ``open_geotiff`` and ``to_geotiff`` forward window / band / max_pixels / tiled / etc. to the backend-specific entry points instead of silently dropping them. @@ -30,8 +30,8 @@ import pytest import xarray as xr -from xrspatial.geotiff import (open_geotiff, read_geotiff_dask, read_geotiff_gpu, read_vrt, - to_geotiff, write_vrt) +from xrspatial.geotiff import (open_geotiff, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + to_geotiff, build_vrt) from .._helpers.markers import gpu_available, requires_gpu, requires_loopback @@ -167,7 +167,7 @@ def _write_vrt_mosaic(dir_path: Path) -> Path: to_geotiff(da, str(p), compression="none", tiled=False) tile_paths.append(str(p)) vrt_path = dir_path / "mosaic_1813.vrt" - write_vrt(str(vrt_path), tile_paths, relative=False, crs=4326) + build_vrt(str(vrt_path), tile_paths, relative=False, crs=4326) return vrt_path @@ -318,14 +318,14 @@ def test_open_geotiff_attrs_match(fixture_factory, fix_id, backend_kwargs): @pytest.mark.parametrize("fix_id", _TIFF_FIXTURES) def test_read_geotiff_dask_matches_open_geotiff(fixture_factory, fix_id): - """``read_geotiff_dask(p)`` byte-matches ``open_geotiff(p, chunks=N)``.""" + """``_read_geotiff_dask(p)`` byte-matches ``open_geotiff(p, chunks=N)``.""" path = fixture_factory(fix_id) via_open = open_geotiff(str(path), chunks=32) - via_direct = read_geotiff_dask(str(path), chunks=32) + via_direct = _read_geotiff_dask(str(path), chunks=32) a = _materialise(via_open).tobytes() b = _materialise(via_direct).tobytes() assert a == b, ( - f"fixture={fix_id}: read_geotiff_dask diverges from " + f"fixture={fix_id}: _read_geotiff_dask diverges from " f"open_geotiff(chunks=32)" ) @@ -333,14 +333,14 @@ def test_read_geotiff_dask_matches_open_geotiff(fixture_factory, fix_id): @_skip_no_gpu @pytest.mark.parametrize("fix_id", _TIFF_FIXTURES) def test_read_geotiff_gpu_matches_open_geotiff(fixture_factory, fix_id): - """``read_geotiff_gpu(p)`` byte-matches ``open_geotiff(p, gpu=True)``.""" + """``_read_geotiff_gpu(p)`` byte-matches ``open_geotiff(p, gpu=True)``.""" path = fixture_factory(fix_id) via_open = open_geotiff(str(path), gpu=True) - via_direct = read_geotiff_gpu(str(path)) + via_direct = _read_geotiff_gpu(str(path)) a = _materialise(via_open).tobytes() b = _materialise(via_direct).tobytes() assert a == b, ( - f"fixture={fix_id}: read_geotiff_gpu diverges from " + f"fixture={fix_id}: _read_geotiff_gpu diverges from " f"open_geotiff(gpu=True)" ) @@ -352,8 +352,8 @@ def test_read_geotiff_gpu_matches_open_geotiff(fixture_factory, fix_id): @pytest.mark.parametrize("backend_kwargs", _BACKENDS) def test_read_vrt_pixel_bytes_match(fixture_factory, backend_kwargs): path = fixture_factory("vrt") - ref = read_vrt(str(path)) - actual = read_vrt(str(path), **backend_kwargs) + ref = _read_vrt(str(path)) + actual = _read_vrt(str(path), **backend_kwargs) assert _materialise(ref).tobytes() == _materialise(actual).tobytes(), ( f"read_vrt backend={backend_kwargs}: pixel bytes differ" ) @@ -362,8 +362,8 @@ def test_read_vrt_pixel_bytes_match(fixture_factory, backend_kwargs): @pytest.mark.parametrize("backend_kwargs", _BACKENDS) def test_read_vrt_coords_match(fixture_factory, backend_kwargs): path = fixture_factory("vrt") - ref = read_vrt(str(path)) - actual = read_vrt(str(path), **backend_kwargs) + ref = _read_vrt(str(path)) + actual = _read_vrt(str(path), **backend_kwargs) for axis in ("y", "x"): ref_c = _coord_view(ref, axis) actual_c = _coord_view(actual, axis) @@ -378,12 +378,12 @@ def test_read_vrt_coords_match(fixture_factory, backend_kwargs): @pytest.mark.parametrize("backend_kwargs", _BACKENDS) def test_open_geotiff_dot_vrt_routes_to_read_vrt(fixture_factory, backend_kwargs): - """``open_geotiff(path.vrt)`` byte-matches ``read_vrt(path)``.""" + """``open_geotiff(path.vrt)`` byte-matches ``_read_vrt(path)``.""" path = fixture_factory("vrt") via_open = open_geotiff(str(path), **backend_kwargs) - via_direct = read_vrt(str(path), **backend_kwargs) + via_direct = _read_vrt(str(path), **backend_kwargs) assert _materialise(via_open).tobytes() == _materialise(via_direct).tobytes(), ( - f"open_geotiff(.vrt) diverges from read_vrt: backend={backend_kwargs}" + f"open_geotiff(.vrt) diverges from _read_vrt: backend={backend_kwargs}" ) @@ -404,7 +404,7 @@ def test_fixture_builders_produce_readable_files(fixture_factory, fix_id): # =========================================================================== # # ``open_geotiff`` and ``to_geotiff`` route to backend-specific entry -# points (``read_geotiff_dask``, ``write_geotiff_gpu``) whose kwarg sets +# points (``_read_geotiff_dask``, ``_write_geotiff_gpu``) whose kwarg sets # were narrower than the dispatcher's. The dispatcher silently dropped # the missing kwargs when it routed to the smaller-API backend; the # fix pins them through. These tests gate that contract. @@ -450,7 +450,7 @@ def small_multiband_tiff_path(tmp_path): def test_read_geotiff_dask_window_clips_region(small_tiff_path): """``window=`` restricts the lazy region; chunks span only the window.""" path, arr = small_tiff_path - da = read_geotiff_dask(path, chunks=2, window=(1, 2, 4, 6)) + da = _read_geotiff_dask(path, chunks=2, window=(1, 2, 4, 6)) assert da.shape == (3, 4) np.testing.assert_array_equal(da.values, arr[1:4, 2:6]) @@ -466,7 +466,7 @@ def test_read_geotiff_dask_window_via_dispatcher(small_tiff_path): def test_read_geotiff_dask_band_selects_single_band(small_multiband_tiff_path): """``band=`` produces a 2D DataArray with the selected band.""" path, arr = small_multiband_tiff_path - da = read_geotiff_dask(path, chunks=4, band=1) + da = _read_geotiff_dask(path, chunks=4, band=1) assert da.ndim == 2 np.testing.assert_array_equal(da.values, arr[:, :, 1]) @@ -492,12 +492,12 @@ def test_read_geotiff_dask_max_pixels_bounds_chunk_not_full_image( path, arr = small_tiff_path # Per-chunk cap is satisfied; full image is not. Should succeed. - da = read_geotiff_dask(path, chunks=2, max_pixels=10) + da = _read_geotiff_dask(path, chunks=2, max_pixels=10) np.testing.assert_array_equal(da.values, arr) # Per-chunk cap is exceeded. The graph builds, but the chunk task # raises the safety-limit error on compute. - da = read_geotiff_dask(path, chunks=4, max_pixels=10) + da = _read_geotiff_dask(path, chunks=4, max_pixels=10) with pytest.raises(ValueError, match="exceed the safety limit"): da.compute() @@ -515,11 +515,11 @@ def test_read_geotiff_dask_max_pixels_chunk_includes_band_count( path, arr = small_multiband_tiff_path # Per-chunk cap satisfied (12 px <= 20) but full image (72) is not. - da = read_geotiff_dask(path, chunks=2, max_pixels=20) + da = _read_geotiff_dask(path, chunks=2, max_pixels=20) np.testing.assert_array_equal(da.values, arr) # Per-chunk cap exceeded (48 px > 20). Graph builds, compute raises. - da = read_geotiff_dask(path, chunks=4, max_pixels=20) + da = _read_geotiff_dask(path, chunks=4, max_pixels=20) with pytest.raises(ValueError, match="exceed the safety limit"): da.compute() @@ -527,7 +527,7 @@ def test_read_geotiff_dask_max_pixels_chunk_includes_band_count( def test_read_geotiff_dask_window_band_combined(small_multiband_tiff_path): """``window`` and ``band`` cooperate.""" path, arr = small_multiband_tiff_path - da = read_geotiff_dask(path, chunks=2, window=(1, 1, 4, 5), band=0) + da = _read_geotiff_dask(path, chunks=2, window=(1, 1, 4, 5), band=0) assert da.shape == (3, 4) np.testing.assert_array_equal(da.values, arr[1:4, 1:5, 0]) @@ -536,32 +536,32 @@ def test_read_geotiff_dask_invalid_window_raises(small_tiff_path): """Out-of-bounds windows fail loudly instead of silently clipping.""" path, _ = small_tiff_path with pytest.raises(ValueError, match="window=.* is outside"): - read_geotiff_dask(path, chunks=2, window=(0, 0, 100, 100)) + _read_geotiff_dask(path, chunks=2, window=(0, 0, 100, 100)) def test_read_geotiff_dask_invalid_band_raises(small_multiband_tiff_path): """Out-of-range band indexes fail with IndexError.""" path, _ = small_multiband_tiff_path with pytest.raises(IndexError, match="band=5 out of range"): - read_geotiff_dask(path, chunks=4, band=5) + _read_geotiff_dask(path, chunks=4, band=5) def test_write_geotiff_gpu_rejects_tiled_false(tmp_path): """The GPU writer is tiled-only; ``tiled=False`` must fail loudly.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu dummy = np.zeros((2, 2), dtype=np.float32) with pytest.raises(ValueError, match="tiled=True"): - write_geotiff_gpu(dummy, str(tmp_path / 'never.tif'), tiled=False) + _write_geotiff_gpu(dummy, str(tmp_path / 'never.tif'), tiled=False) def test_write_geotiff_gpu_rejects_nonzero_max_z_error(tmp_path): """LERC budget is not implementable on the GPU path.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu dummy = np.zeros((2, 2), dtype=np.float32) with pytest.raises(ValueError, match="max_z_error is not supported"): - write_geotiff_gpu(dummy, str(tmp_path / 'never.tif'), max_z_error=1.0) + _write_geotiff_gpu(dummy, str(tmp_path / 'never.tif'), max_z_error=1.0) @_skip_no_gpu @@ -569,14 +569,14 @@ def test_write_geotiff_gpu_accepts_streaming_buffer_bytes_as_noop(tmp_path): """``streaming_buffer_bytes`` is accepted for API parity (no-op).""" import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu arr = cupy.arange(16, dtype=cupy.float32).reshape(4, 4) da = xr.DataArray(arr, dims=['y', 'x'], coords={'y': np.arange(4, dtype=np.float64), 'x': np.arange(4, dtype=np.float64)}) p = tmp_path / 'kwarg_parity_streaming.tif' - write_geotiff_gpu(da, str(p), streaming_buffer_bytes=4096, tile_size=16) + _write_geotiff_gpu(da, str(p), streaming_buffer_bytes=4096, tile_size=16) rd = open_geotiff(str(p)) np.testing.assert_array_equal(rd.values, arr.get()) diff --git a/xrspatial/geotiff/tests/parity/test_reference.py b/xrspatial/geotiff/tests/parity/test_reference.py index 96b89b662..fbab5bcf1 100644 --- a/xrspatial/geotiff/tests/parity/test_reference.py +++ b/xrspatial/geotiff/tests/parity/test_reference.py @@ -27,8 +27,8 @@ import pytest import xarray as xr -from xrspatial.geotiff import (open_geotiff, read_geotiff_dask, read_geotiff_gpu, to_geotiff, - write_geotiff_gpu) +from xrspatial.geotiff import (open_geotiff, _read_geotiff_dask, _read_geotiff_gpu, to_geotiff, + _write_geotiff_gpu) from .._helpers.markers import requires_gpu @@ -69,9 +69,9 @@ def test_dask_numpy_backend(self, single_pixel_path): np.testing.assert_array_equal(computed.values, arr) def test_read_geotiff_dask_direct(self, single_pixel_path): - """The explicit ``read_geotiff_dask`` entry point matches dispatch.""" + """The explicit ``_read_geotiff_dask`` entry point matches dispatch.""" path, arr = single_pixel_path - result = read_geotiff_dask(path, chunks=8) + result = _read_geotiff_dask(path, chunks=8) assert result.shape == (1, 1) np.testing.assert_array_equal(result.compute().values, arr) @@ -84,9 +84,9 @@ def test_gpu_backend(self, single_pixel_path): @requires_gpu def test_read_geotiff_gpu_direct(self, single_pixel_path): - """The explicit ``read_geotiff_gpu`` entry point matches dispatch.""" + """The explicit ``_read_geotiff_gpu`` entry point matches dispatch.""" path, arr = single_pixel_path - result = read_geotiff_gpu(path) + result = _read_geotiff_gpu(path) assert result.shape == (1, 1) np.testing.assert_array_equal(result.data.get(), arr) @@ -185,7 +185,7 @@ def test_dask_cupy_backend(self, single_column_path): @requires_gpu class TestGpuWriterDegenerateShapes: - """``write_geotiff_gpu`` must accept 1-pixel, 1-row, and 1-column inputs. + """``_write_geotiff_gpu`` must accept 1-pixel, 1-row, and 1-column inputs. The GPU writer's tile-encoding path uses an internal grid sizing helper that fell back to host code for shapes smaller than the @@ -198,7 +198,7 @@ def test_single_pixel_round_trip(self, tmp_path): arr = cupy.array([[42.0]], dtype=cupy.float32) da_gpu = xr.DataArray(arr, dims=["y", "x"]) p = str(tmp_path / "gpu_1x1.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) result = open_geotiff(p) assert result.shape == (1, 1) @@ -210,7 +210,7 @@ def test_single_row_round_trip(self, tmp_path): arr = cupy.asarray(arr_np) da_gpu = xr.DataArray(arr, dims=["y", "x"]) p = str(tmp_path / "gpu_1xN.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) result = open_geotiff(p) assert result.shape == (1, 10) @@ -222,7 +222,7 @@ def test_single_column_round_trip(self, tmp_path): arr = cupy.asarray(arr_np) da_gpu = xr.DataArray(arr, dims=["y", "x"]) p = str(tmp_path / "gpu_Nx1.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) result = open_geotiff(p) assert result.shape == (10, 1) diff --git a/xrspatial/geotiff/tests/parity/test_signature_contract.py b/xrspatial/geotiff/tests/parity/test_signature_contract.py index 54af35b7c..efb963053 100644 --- a/xrspatial/geotiff/tests/parity/test_signature_contract.py +++ b/xrspatial/geotiff/tests/parity/test_signature_contract.py @@ -6,14 +6,14 @@ Three sections, each a former top-level file: Section 1 -- Writer signature / docstring parity - ``write_vrt`` exposes its documented kwargs through an explicit - signature (no ``**kwargs`` catch-all), ``write_geotiff_gpu`` lists + ``build_vrt`` exposes its documented kwargs through an explicit + signature (no ``**kwargs`` catch-all), ``_write_geotiff_gpu`` lists ``'cubic'`` in its ``overview_resampling`` docstring, and its ``data`` parameter carries the same type hint as ``to_geotiff``. Section 2 -- Read entry-point docstring/param parity - Every signature kwarg on ``open_geotiff`` / ``read_geotiff_dask`` / - ``read_geotiff_gpu`` / ``read_vrt`` has a matching numpy-style + Every signature kwarg on ``open_geotiff`` / ``_read_geotiff_dask`` / + ``_read_geotiff_gpu`` / ``_read_vrt`` has a matching numpy-style Parameters entry, and every Parameters entry maps to a real kwarg. Section 3 -- Release-contract tier parity @@ -35,8 +35,9 @@ import pytest import xarray as xr -from xrspatial.geotiff import (SUPPORTED_FEATURES, open_geotiff, read_geotiff_dask, - read_geotiff_gpu, read_vrt, to_geotiff, write_geotiff_gpu, write_vrt) +from xrspatial.geotiff import (SUPPORTED_FEATURES, build_vrt, open_geotiff, to_geotiff, + _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + _write_geotiff_gpu) from .._helpers.markers import requires_gpu @@ -45,24 +46,24 @@ # =========================================================================== # # Three drifts this section guards against: -# ``write_vrt`` swallowed every kwarg into ``**kwargs`` so the documented +# ``build_vrt`` swallowed every kwarg into ``**kwargs`` so the documented # ``relative`` / ``crs`` / ``nodata`` were invisible to ``inspect.signature``; -# ``write_geotiff_gpu``'s ``overview_resampling`` docstring omitted -# ``'cubic'``; and ``write_geotiff_gpu(data, ...)`` lacked the type hint +# ``_write_geotiff_gpu``'s ``overview_resampling`` docstring omitted +# ``'cubic'``; and ``_write_geotiff_gpu(data, ...)`` lacked the type hint # ``to_geotiff(data, ...)`` carries. def test_write_vrt_signature_exposes_documented_kwargs(): - """``inspect.signature(write_vrt)`` reports the four accepted kwargs. + """``inspect.signature(build_vrt)`` reports the four accepted kwargs. When the public wrapper used ``**kwargs``, ``inspect.signature`` only saw ``vrt_path`` and ``source_files``. ``crs`` was added for - parity with ``to_geotiff`` / ``write_geotiff_gpu`` while keeping the + parity with ``to_geotiff`` / ``_write_geotiff_gpu`` while keeping the historic ``crs_wkt`` as a deprecated alias (sentinel default so the deprecation shim can tell "user passed nothing" from "user passed crs_wkt=None"). """ - sig = inspect.signature(write_vrt) + sig = inspect.signature(build_vrt) params = sig.parameters assert 'relative' in params assert 'crs' in params # canonical kwarg @@ -70,7 +71,7 @@ def test_write_vrt_signature_exposes_documented_kwargs(): assert 'nodata' in params assert params['relative'].default is True # ``crs`` is the new canonical kwarg; default None means "pick from - # the first source", matching to_geotiff / write_geotiff_gpu. + # the first source", matching to_geotiff / _write_geotiff_gpu. assert params['crs'].default is None # ``crs_wkt`` carries a sentinel default so the deprecation shim # can distinguish "user passed nothing" (no warning) from "user @@ -99,7 +100,7 @@ def test_write_vrt_unknown_kwarg_rejected_at_public_level(tmp_path): to_geotiff(da, tif_path) with pytest.raises(TypeError, match='typo_kwarg'): - write_vrt(str(tmp_path / 't.vrt'), [tif_path], typo_kwarg=1) + build_vrt(str(tmp_path / 't.vrt'), [tif_path], typo_kwarg=1) def test_write_vrt_accepts_documented_kwargs(tmp_path): @@ -118,7 +119,7 @@ def test_write_vrt_accepts_documented_kwargs(tmp_path): to_geotiff(da, tif_path) vrt_path = str(tmp_path / 't.vrt') - out = write_vrt( + out = build_vrt( vrt_path, [tif_path], relative=False, crs=None, nodata=-9999.0, ) @@ -130,7 +131,7 @@ def test_write_geotiff_gpu_docstring_lists_cubic(): """``overview_resampling`` docstring includes ``'cubic'`` so it matches ``to_geotiff`` and the underlying ``make_overview_gpu``. """ - doc = write_geotiff_gpu.__doc__ + doc = _write_geotiff_gpu.__doc__ assert doc is not None # Find the overview_resampling block assert 'overview_resampling' in doc @@ -149,7 +150,7 @@ def test_write_geotiff_gpu_data_has_type_hint(): and the test suite exercises that path (e.g. ``test_backend_kwarg_parity_1561.py`` passes a numpy ``dummy``). """ - sig = inspect.signature(write_geotiff_gpu) + sig = inspect.signature(_write_geotiff_gpu) data_param = sig.parameters['data'] assert data_param.annotation is not inspect.Parameter.empty # The annotation is a forward reference under ``from __future__ import @@ -177,7 +178,7 @@ def test_write_geotiff_gpu_cubic_overview_round_trip(tmp_path): coords={'y': np.arange(256.0, 0, -1), 'x': np.arange(256.0)}, ) path = str(tmp_path / 'cog.tif') - write_geotiff_gpu( + _write_geotiff_gpu( da_gpu, path, cog=True, tile_size=64, overview_resampling='cubic', ) @@ -198,9 +199,9 @@ def test_write_geotiff_gpu_cubic_overview_round_trip(tmp_path): READ_ENTRY_POINTS = ( open_geotiff, - read_geotiff_dask, - read_geotiff_gpu, - read_vrt, + _read_geotiff_dask, + _read_geotiff_gpu, + _read_vrt, ) diff --git a/xrspatial/geotiff/tests/read/test_basic.py b/xrspatial/geotiff/tests/read/test_basic.py index fe795e1f1..61d9a59f6 100644 --- a/xrspatial/geotiff/tests/read/test_basic.py +++ b/xrspatial/geotiff/tests/read/test_basic.py @@ -19,7 +19,7 @@ import xarray as xr from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, to_geotiff -from xrspatial.geotiff import write_vrt as _write_vrt_1810 +from xrspatial.geotiff import build_vrt as _write_vrt_1810 from xrspatial.geotiff._dtypes import tiff_dtype_to_numpy from xrspatial.geotiff._geotags import RASTER_PIXEL_IS_POINT, TAG_GEO_ASCII_PARAMS, extract_geo_info from xrspatial.geotiff._header import parse_all_ifds, parse_header @@ -101,7 +101,7 @@ class TestBandValidationBackendParity: def test_negative_band(self, multiband_tiff_path): """Both paths raise the same error for ``band=-1``.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask from xrspatial.geotiff._reader import read_to_array path, _ = multiband_tiff_path @@ -109,14 +109,14 @@ def test_negative_band(self, multiband_tiff_path): with pytest.raises(IndexError) as eager_exc: read_to_array(path, band=-1) with pytest.raises(IndexError) as dask_exc: - read_geotiff_dask(path, chunks=4, band=-1) + _read_geotiff_dask(path, chunks=4, band=-1) assert "band=-1 out of range" in str(eager_exc.value) assert "band=-1 out of range" in str(dask_exc.value) def test_band_equal_to_samples(self, multiband_tiff_path): """Both paths agree on the off-by-one rejection.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask from xrspatial.geotiff._reader import read_to_array path, _ = multiband_tiff_path @@ -124,7 +124,7 @@ def test_band_equal_to_samples(self, multiband_tiff_path): with pytest.raises(IndexError) as eager_exc: read_to_array(path, band=3) with pytest.raises(IndexError) as dask_exc: - read_geotiff_dask(path, chunks=4, band=3) + _read_geotiff_dask(path, chunks=4, band=3) assert "band=3 out of range" in str(eager_exc.value) assert "band=3 out of range" in str(dask_exc.value) @@ -135,7 +135,7 @@ def test_band_equal_to_samples(self, multiband_tiff_path): # ============================================================================= # # ``open_geotiff`` must accept ``missing_sources`` and forward it to -# ``read_vrt`` when the source is a VRT. ``read_vrt`` exposes a +# ``_read_vrt`` when the source is a VRT. ``_read_vrt`` exposes a # ``missing_sources='warn'|'raise'`` policy kwarg; the documented # dispatcher entry point ``open_geotiff`` must expose it too rather than # silently dropping the backend kwarg. @@ -961,7 +961,7 @@ def seek(self, *a, **k): class TestWriteGeotiffGpuBytesIO: - """Regression coverage for ``write_geotiff_gpu`` file-like behaviour. + """Regression coverage for ``_write_geotiff_gpu`` file-like behaviour. ``to_geotiff(gpu=True, ...)`` always rejects BytesIO destinations paired with ``cog=True`` (the auto-dispatch path's existing guard). The explicit @@ -980,11 +980,11 @@ def test_cog_with_bytesio_rejected_1652(self): coords={'y': np.arange(64.0), 'x': np.arange(64.0)}, attrs={'crs': 4326}, ) - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu buf = io.BytesIO() with pytest.raises(ValueError, match='cog=True'): - write_geotiff_gpu(da, buf, cog=True) + _write_geotiff_gpu(da, buf, cog=True) @_gpu_only def test_cog_with_bytesio_error_matches_to_geotiff_1652(self): @@ -998,10 +998,10 @@ def test_cog_with_bytesio_error_matches_to_geotiff_1652(self): coords={'y': np.arange(64.0), 'x': np.arange(64.0)}, attrs={'crs': 4326}, ) - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu # to_geotiff's canonical message; mirrored verbatim in - # write_geotiff_gpu's gate. + # _write_geotiff_gpu's gate. expected = ( "cog=True is not supported for file-like destinations. " "Pass a string path or write to BytesIO without cog=True." @@ -1009,7 +1009,7 @@ def test_cog_with_bytesio_error_matches_to_geotiff_1652(self): buf = io.BytesIO() with pytest.raises(ValueError) as exc_info: - write_geotiff_gpu(da, buf, cog=True) + _write_geotiff_gpu(da, buf, cog=True) assert str(exc_info.value) == expected # And the CPU writer raises the same string for parity. @@ -1028,10 +1028,10 @@ def test_invalid_path_type_raises_typeerror_1652(self): coords={'y': np.arange(64.0), 'x': np.arange(64.0)}, attrs={'crs': 4326}, ) - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu with pytest.raises(TypeError, match="path must be a str"): - write_geotiff_gpu(da, 42) # int is neither str nor file-like + _write_geotiff_gpu(da, 42) # int is neither str nor file-like @_gpu_only def test_non_cog_bytesio_still_works_1652(self): @@ -1043,12 +1043,12 @@ def test_non_cog_bytesio_still_works_1652(self): coords={'y': np.arange(64.0), 'x': np.arange(64.0)}, attrs={'crs': 4326}, ) - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu buf = io.BytesIO() # Non-cog file-like write is still supported on the explicit GPU # writer; only cog=True is gated. - write_geotiff_gpu(da, buf) + _write_geotiff_gpu(da, buf) assert len(buf.getvalue()) > 0 # Verify it round-trips through open_geotiff diff --git a/xrspatial/geotiff/tests/read/test_coords.py b/xrspatial/geotiff/tests/read/test_coords.py index 52de8a4f3..f4a812844 100644 --- a/xrspatial/geotiff/tests/read/test_coords.py +++ b/xrspatial/geotiff/tests/read/test_coords.py @@ -943,7 +943,7 @@ def test_write_geotiff_gpu_roundtrip_3d(self, tmp_path): """ import cupy as cp - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu np_arr = np.arange(10 * 20 * 3, dtype=np.uint8).reshape(10, 20, 3) da = xr.DataArray( @@ -956,7 +956,7 @@ def test_write_geotiff_gpu_roundtrip_3d(self, tmp_path): }, ) p = str(tmp_path / 'roundtrip_3d_gpu.tif') - write_geotiff_gpu(da, p) + _write_geotiff_gpu(da, p) rt = open_geotiff(p) pw = abs(float(rt.x.values[1] - rt.x.values[0])) assert pw > 1.5, ( diff --git a/xrspatial/geotiff/tests/read/test_crs.py b/xrspatial/geotiff/tests/read/test_crs.py index 3004000b2..e88492409 100644 --- a/xrspatial/geotiff/tests/read/test_crs.py +++ b/xrspatial/geotiff/tests/read/test_crs.py @@ -552,7 +552,7 @@ def test_open_geotiff_rotated_with_crs_drops_crs_gpu(tmp_path, chunks): """``allow_rotated=True`` on the GPU eager + dask+CuPy paths drops ``crs`` / ``crs_wkt``. - ``read_geotiff_gpu`` must forward ``allow_rotated`` when it falls back + ``_read_geotiff_gpu`` must forward ``allow_rotated`` when it falls back to ``_read_to_array`` for the stripped layout (and the three tiled CPU-fallback sites); otherwise the CPU re-read raises. The dask+CuPy path routes through the dask backend with ``cupy.asarray`` mapped over diff --git a/xrspatial/geotiff/tests/read/test_dtypes.py b/xrspatial/geotiff/tests/read/test_dtypes.py index 5c145b5c5..b5258ee29 100644 --- a/xrspatial/geotiff/tests/read/test_dtypes.py +++ b/xrspatial/geotiff/tests/read/test_dtypes.py @@ -5,7 +5,7 @@ * The ``dtype=`` kwarg on ``open_geotiff`` (eager + dask, float -> float / int -> int casts, float -> int rejection). * IEEE half-precision auto-promotion to float32 on read (eager + dask). -* The same float16 promotion on ``read_geotiff_gpu`` and +* The same float16 promotion on ``_read_geotiff_gpu`` and ``open_geotiff(gpu=True)``. """ from __future__ import annotations @@ -14,7 +14,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import open_geotiff, read_geotiff_dask, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_geotiff_dask, to_geotiff from xrspatial.geotiff._dtypes import (SAMPLE_FORMAT_FLOAT, SAMPLE_FORMAT_INT, SAMPLE_FORMAT_UINT, tiff_dtype_to_numpy, tiff_storage_dtype) @@ -247,7 +247,7 @@ def test_open_geotiff_returns_float32(self, float16_tif): def test_open_geotiff_dask_returns_float32(self, float16_tif): path, arr = float16_tif - result = read_geotiff_dask(str(path), chunks=2) + result = _read_geotiff_dask(str(path), chunks=2) assert result.dtype == np.float32 np.testing.assert_array_equal( result.compute().values, arr.astype(np.float32)) @@ -310,16 +310,16 @@ def test_uint16_still_uint16(self, tmp_path): class TestEagerGPUReadFloat16: - """``read_geotiff_gpu`` returns float32 for stripped float16 input.""" + """``_read_geotiff_gpu`` returns float32 for stripped float16 input.""" @_gpu_only def test_read_geotiff_gpu_stripped_returns_float32( self, float16_stripped_tif ): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_stripped_tif - result = read_geotiff_gpu(str(path)) + result = _read_geotiff_gpu(str(path)) assert result.dtype == np.float32, ( f"GPU read of float16 must return float32, got {result.dtype}" ) @@ -330,10 +330,10 @@ def test_read_geotiff_gpu_stripped_returns_float32( def test_read_geotiff_gpu_tiled_returns_float32( self, float16_tiled_tif ): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_tiled_tif - result = read_geotiff_gpu(str(path)) + result = _read_geotiff_gpu(str(path)) assert result.dtype == np.float32 np.testing.assert_array_equal( result.data.get(), arr.astype(np.float32)) @@ -342,10 +342,10 @@ def test_read_geotiff_gpu_tiled_returns_float32( def test_read_geotiff_gpu_tiled_uncompressed_returns_float32( self, float16_tiled_uncompressed_tif ): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_tiled_uncompressed_tif - result = read_geotiff_gpu(str(path)) + result = _read_geotiff_gpu(str(path)) assert result.dtype == np.float32 np.testing.assert_array_equal( result.data.get(), arr.astype(np.float32)) @@ -365,10 +365,10 @@ class TestGPUWindowedFloat16: @_gpu_only def test_read_geotiff_gpu_windowed_stripped(self, float16_stripped_tif): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_stripped_tif - result = read_geotiff_gpu(str(path), window=(0, 0, 2, 2)) + result = _read_geotiff_gpu(str(path), window=(0, 0, 2, 2)) assert result.dtype == np.float32 assert result.shape == (2, 2) np.testing.assert_array_equal( @@ -376,10 +376,10 @@ def test_read_geotiff_gpu_windowed_stripped(self, float16_stripped_tif): @_gpu_only def test_read_geotiff_gpu_windowed_tiled(self, float16_tiled_tif): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_tiled_tif - result = read_geotiff_gpu(str(path), window=(0, 0, 8, 8)) + result = _read_geotiff_gpu(str(path), window=(0, 0, 8, 8)) assert result.dtype == np.float32 assert result.shape == (8, 8) np.testing.assert_array_equal( @@ -402,11 +402,11 @@ def test_dask_gpu_tiled_float16(self, float16_tiled_tif): @_gpu_only def test_read_geotiff_gpu_chunks_kwarg_float16(self, float16_tiled_tif): - """``read_geotiff_gpu(chunks=)`` also routes correctly.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(chunks=)`` also routes correctly.""" + from xrspatial.geotiff import _read_geotiff_gpu path, arr = float16_tiled_tif - result = read_geotiff_gpu(str(path), chunks=8) + result = _read_geotiff_gpu(str(path), chunks=8) assert result.dtype == np.float32 computed = result.compute() np.testing.assert_array_equal( @@ -500,7 +500,7 @@ def test_eager_numpy_equals_dask_gpu(self, float16_tiled_tif): @_gpu_only def test_dask_numpy_equals_dask_gpu(self, float16_tiled_tif): path, _ = float16_tiled_tif - dask_cpu = read_geotiff_dask(str(path), chunks=8).compute() + dask_cpu = _read_geotiff_dask(str(path), chunks=8).compute() dask_gpu = open_geotiff(str(path), chunks=8, gpu=True).compute() np.testing.assert_array_equal( @@ -515,14 +515,14 @@ def test_predictor3_float16_gpu_round_trip(self, tmp_path): tifffile = pytest.importorskip("tifffile") pytest.importorskip("imagecodecs") # required for predictor=3 - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.linspace(-1.0, 1.0, 16).astype(np.float16).reshape(4, 4) path = tmp_path / "pred3_f16.tif" tifffile.imwrite( str(path), arr, predictor=3, compression="deflate") - result = read_geotiff_gpu(str(path)) + result = _read_geotiff_gpu(str(path)) assert result.dtype == np.float32 np.testing.assert_array_equal( result.data.get(), arr.astype(np.float32)) diff --git a/xrspatial/geotiff/tests/read/test_endianness.py b/xrspatial/geotiff/tests/read/test_endianness.py index b66fff1c3..9b79a32ff 100644 --- a/xrspatial/geotiff/tests/read/test_endianness.py +++ b/xrspatial/geotiff/tests/read/test_endianness.py @@ -1,6 +1,6 @@ """Big-endian / little-endian GeoTIFF reader paths. -Big-endian multi-byte TIFFs read via ``read_geotiff_gpu`` once crashed +Big-endian multi-byte TIFFs read via ``_read_geotiff_gpu`` once crashed inside the GPU decode pipeline because ``cupy.ndarray`` does not expose ``byteswap()``. The dispatcher caught the error and silently fell back to CPU, so results stayed correct but the GPU fast path was lost. These @@ -29,7 +29,7 @@ def test_read_geotiff_gpu_big_endian_multibyte(tmp_path, dtype): import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu from xrspatial.geotiff._reader import read_to_array rng = np.random.RandomState(20260507) @@ -50,7 +50,7 @@ def test_read_geotiff_gpu_big_endian_multibyte(tmp_path, dtype): f"CPU baseline drifted from native dtype: got {cpu.dtype}" ) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray), ( "expected cupy-backed DataArray, got " @@ -75,7 +75,7 @@ def test_read_geotiff_gpu_big_endian_uncompressed(tmp_path): import cupy import tifffile - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu rng = np.random.RandomState(20260507) arr = rng.randint(0, 60000, size=(32, 48), dtype=np.uint16) @@ -85,7 +85,7 @@ def test_read_geotiff_gpu_big_endian_uncompressed(tmp_path): str(path), arr, byteorder=">", compression=None, tile=(16, 16), ) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray), ( "expected cupy-backed DataArray; GPU path may have fallen back" ) diff --git a/xrspatial/geotiff/tests/read/test_georef.py b/xrspatial/geotiff/tests/read/test_georef.py index 999fd6532..a5685531c 100644 --- a/xrspatial/geotiff/tests/read/test_georef.py +++ b/xrspatial/geotiff/tests/read/test_georef.py @@ -41,7 +41,7 @@ import xarray as xr from xrspatial.geotiff import (NonUniformCoordsError, _coords_to_transform, _transform_from_attr, - open_geotiff, read_vrt, to_geotiff, write_geotiff_gpu) + open_geotiff, _read_vrt, to_geotiff, _write_geotiff_gpu) from xrspatial.geotiff._attrs import (_ATTRS_CONTRACT_VERSION, GEOREF_STATUS_CRS_ONLY, GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_ROTATED_DROPPED, GEOREF_STATUS_TRANSFORM_ONLY, @@ -876,7 +876,7 @@ def test_vrt_full_status(tmp_path): crs_wkt='EPSG:4326', geo_transform='100.0, 1.0, 0.0, 200.5, 0.0, -1.0', ) - rd = read_vrt(str(vrt)) + rd = _read_vrt(str(vrt)) assert rd.attrs[_STATUS_KEY] == GEOREF_STATUS_FULL @@ -889,7 +889,7 @@ def test_vrt_crs_only_status(tmp_path): vrt, os.path.basename(src), height=4, width=4, crs_wkt='EPSG:4326', ) - rd = read_vrt(str(vrt)) + rd = _read_vrt(str(vrt)) assert rd.attrs[_STATUS_KEY] == GEOREF_STATUS_CRS_ONLY @@ -899,7 +899,7 @@ def test_vrt_none_status(tmp_path): _make_none_tiff(str(src)) vrt = tmp_path / "georef_status_2136_vrt_none.vrt" _write_vrt_georef_status(vrt, os.path.basename(src), height=4, width=4) - rd = read_vrt(str(vrt)) + rd = _read_vrt(str(vrt)) assert rd.attrs[_STATUS_KEY] == GEOREF_STATUS_NONE @@ -912,7 +912,7 @@ def test_vrt_full_status_chunked(tmp_path): crs_wkt='EPSG:4326', geo_transform='100.0, 1.0, 0.0, 200.5, 0.0, -1.0', ) - rd = read_vrt(str(vrt), chunks=2) + rd = _read_vrt(str(vrt), chunks=2) assert rd.attrs[_STATUS_KEY] == GEOREF_STATUS_FULL @@ -1332,7 +1332,7 @@ def test_dask_cupy_windowed_integer_coords(self, no_georef_path_1710): def test_offset_window_integer_coords(self, no_georef_path_1710): """GPU windowed read at a non-zero origin: the stripped-GPU - fallback in ``read_geotiff_gpu`` must check ``has_georef`` rather + fallback in ``_read_geotiff_gpu`` must check ``has_georef`` rather than ``t is None``; otherwise a non-georef TIFF emits ``[-0.5, -1.5, ...]`` via the unit ``GeoTransform`` placeholder. Pin the contract that the offset @@ -2111,7 +2111,7 @@ def test_1xN_round_trip_preserves_transform(self, tmp_path): da_gpu = da_cpu.copy(data=cupy.asarray(da_cpu.values)) da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "georef_1xN_gpu_1945.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) _assert_round_trip_1945(p, da_cpu) def test_Nx1_round_trip_preserves_transform(self, tmp_path): @@ -2120,7 +2120,7 @@ def test_Nx1_round_trip_preserves_transform(self, tmp_path): da_gpu = da_cpu.copy(data=cupy.asarray(da_cpu.values)) da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "georef_Nx1_gpu_1945.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) _assert_round_trip_1945(p, da_cpu) def test_1x1_with_transform_attr_round_trips(self, tmp_path): @@ -2129,7 +2129,7 @@ def test_1x1_with_transform_attr_round_trips(self, tmp_path): da_gpu = da_cpu.copy(data=cupy.asarray(da_cpu.values)) da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "georef_1x1_gpu_1945.tif") - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) _assert_round_trip_1945(p, da_cpu) def test_1x1_without_transform_raises(self, tmp_path): @@ -2139,7 +2139,7 @@ def test_1x1_without_transform_raises(self, tmp_path): da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "georef_1x1_no_tx_gpu_1945.tif") with pytest.raises(ValueError, match="(?i)pixel size|transform"): - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) @_gpu_only diff --git a/xrspatial/geotiff/tests/read/test_nodata.py b/xrspatial/geotiff/tests/read/test_nodata.py index c6592d125..47f595175 100644 --- a/xrspatial/geotiff/tests/read/test_nodata.py +++ b/xrspatial/geotiff/tests/read/test_nodata.py @@ -33,7 +33,7 @@ import xarray as xr from xrspatial.geotiff import (GeoTIFFAmbiguousMetadataError, InvalidIntegerNodataError, - open_geotiff, read_geotiff_dask, read_vrt, to_geotiff) + open_geotiff, _read_geotiff_dask, _read_vrt, to_geotiff) from xrspatial.geotiff._attrs import _finalize_lazy_read_attrs, _validate_read_geo_info from xrspatial.geotiff._backends import _gpu_helpers from xrspatial.geotiff._errors import MixedBandMetadataError @@ -352,10 +352,10 @@ def test_no_nodata_attrs_means_no_tag(tmp_path, arr_with_sentinel_1582): ]) def test_gpu_writer_resolves_alias(tmp_path, arr_with_sentinel_1582, attr_key, attr_value): - """The GPU write path (write_geotiff_gpu) honours the same aliases.""" + """The GPU write path (_write_geotiff_gpu) honours the same aliases.""" import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu da = xr.DataArray( cupy.asarray(arr_with_sentinel_1582), @@ -365,7 +365,7 @@ def test_gpu_writer_resolves_alias(tmp_path, arr_with_sentinel_1582, attrs={"crs": 4326, attr_key: attr_value}, ) out = str(tmp_path / f"gpu_{attr_key}.tif") - write_geotiff_gpu(da, out, compression="none") + _write_geotiff_gpu(da, out, compression="none") rd = open_geotiff(out) assert rd.attrs.get("nodata") == _SENTINEL_1582 @@ -474,7 +474,7 @@ def test_dask_mask_nodata_false_reports_false(tmp_path): path = str(tmp_path / "tmp_2092_dask_unmasked.tif") _make_float_raster_with_nodata_2092(path) - out = read_geotiff_dask(path, chunks=2, mask_nodata=False) + out = _read_geotiff_dask(path, chunks=2, mask_nodata=False) assert out.attrs.get('masked_nodata') is False computed = out.values assert -9999.0 in computed @@ -484,7 +484,7 @@ def test_dask_mask_nodata_true_reports_true(tmp_path): path = str(tmp_path / "tmp_2092_dask_masked.tif") _make_float_raster_with_nodata_2092(path) - out = read_geotiff_dask(path, chunks=2) + out = _read_geotiff_dask(path, chunks=2) assert out.attrs.get('masked_nodata') is True computed = out.values assert np.isnan(computed).sum() == 2 @@ -505,7 +505,7 @@ def test_dask_explicit_float_dtype_mask_off_reports_false(tmp_path): path = str(tmp_path / "tmp_2092_dask_int_to_float_unmasked.tif") to_geotiff(da, path) - out = read_geotiff_dask( + out = _read_geotiff_dask( path, chunks=2, mask_nodata=False, dtype=np.float64, ) assert out.dtype == np.float64 @@ -606,23 +606,23 @@ def test_vrt_int_source_mask_off_with_float_cast_reports_false(tmp_path): @_gpu_only def test_gpu_mask_nodata_false_reports_false(tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tmp_2092_gpu_unmasked.tif") _make_float_raster_with_nodata_2092(path) - out = read_geotiff_gpu(path, mask_nodata=False) + out = _read_geotiff_gpu(path, mask_nodata=False) assert out.attrs.get('masked_nodata') is False @_gpu_only def test_gpu_mask_nodata_true_reports_true(tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tmp_2092_gpu_masked.tif") _make_float_raster_with_nodata_2092(path) - out = read_geotiff_gpu(path) + out = _read_geotiff_gpu(path) assert out.attrs.get('masked_nodata') is True @@ -782,7 +782,7 @@ def test_dask_leaves_pixels_present_unset(tmp_path): ``nodata_pixels_present`` stays unset by design.""" path = str(tmp_path / "tmp_2135_dask_present.tif") _make_float_raster_2135(path) - out = read_geotiff_dask(path, chunks=2) + out = _read_geotiff_dask(path, chunks=2) assert out.attrs.get('masked_nodata') is True assert 'nodata_pixels_present' not in out.attrs @@ -791,7 +791,7 @@ def test_dask_dtype_cast_records_target(tmp_path): """Dask path emits ``nodata_dtype_cast`` when caller passes dtype=.""" path = str(tmp_path / "tmp_2135_dask_cast.tif") _make_int_raster_2135(path) - out = read_geotiff_dask( + out = _read_geotiff_dask( path, chunks=2, mask_nodata=False, dtype=np.float64, ) assert out.attrs.get('masked_nodata') is False @@ -803,7 +803,7 @@ def test_dask_no_dtype_cast_attr_absent(tmp_path): """Dask path without dtype=: nodata_dtype_cast absent.""" path = str(tmp_path / "tmp_2135_dask_no_cast.tif") _make_float_raster_2135(path) - out = read_geotiff_dask(path, chunks=2) + out = _read_geotiff_dask(path, chunks=2) assert 'nodata_dtype_cast' not in out.attrs @@ -888,22 +888,22 @@ def test_vrt_dtype_cast_records_target(tmp_path): @_gpu_only def test_gpu_float_sentinel_present_masked(tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tmp_2135_gpu_float_present.tif") _make_float_raster_2135(path) - out = read_geotiff_gpu(path) + out = _read_geotiff_gpu(path) assert out.attrs.get('masked_nodata') is True assert out.attrs.get('nodata_pixels_present') is True @_gpu_only def test_gpu_int_sentinel_absent(tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tmp_2135_gpu_int_absent.tif") _make_int_raster_2135(path, plant_sentinel=False) - out = read_geotiff_gpu(path) + out = _read_geotiff_gpu(path) # No sentinel pixel: helper short-circuits, buffer stays int. assert out.attrs.get('masked_nodata') is False assert out.attrs.get('nodata_pixels_present') is False @@ -911,11 +911,11 @@ def test_gpu_int_sentinel_absent(tmp_path): @_gpu_only def test_gpu_dtype_cast_records_target(tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tmp_2135_gpu_cast.tif") _make_int_raster_2135(path) - out = read_geotiff_gpu(path, mask_nodata=False, dtype=np.float64) + out = _read_geotiff_gpu(path, mask_nodata=False, dtype=np.float64) assert out.attrs.get('nodata_dtype_cast') == 'float64' @@ -1050,7 +1050,7 @@ def test_read_geotiff_dask_int_nodata_nan(tmp_path): ``allow_invalid_nodata`` opt-in. """ path = _build_uint16_tiff_1774('nan', tmp_path) - da = read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) + da = _read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) # effective_dtype stays uint16 because the sentinel is non-finite assert da.dtype == np.uint16 np.testing.assert_array_equal(da.compute().values, [[10, 20], [30, 40]]) @@ -1063,7 +1063,7 @@ def test_read_geotiff_dask_int_nodata_inf(tmp_path): ``allow_invalid_nodata`` opt-in. """ path = _build_uint16_tiff_1774('inf', tmp_path) - da = read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) + da = _read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.compute().values, [[10, 20], [30, 40]]) assert np.isinf(da.attrs['nodata']) @@ -1145,7 +1145,7 @@ def test_read_geotiff_dask_int_nodata_fractional_noop(tmp_path): ``allow_invalid_nodata`` opt-in. """ path = _build_uint16_tiff_1774('30.5', tmp_path) - da = read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) + da = _read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) # effective_dtype stays uint16 because the sentinel is fractional assert da.dtype == np.uint16 computed = da.compute().values @@ -1913,7 +1913,7 @@ def test_eager_masks_int_sentinel_to_nan(self, int_tif): def test_dask_matches_eager(self, int_tif): eager = open_geotiff(int_tif) - lazy = read_geotiff_dask(int_tif, chunks=2) + lazy = _read_geotiff_dask(int_tif, chunks=2) # Same on-disk sentinel propagated. assert lazy.attrs["nodata"] == eager.attrs["nodata"] # Same lifecycle decision: dask graph promoted to float64. @@ -1924,10 +1924,10 @@ def test_dask_matches_eager(self, int_tif): @_gpu_only def test_gpu_matches_eager(self, int_tif): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu eager = open_geotiff(int_tif) - gpu = read_geotiff_gpu(int_tif) + gpu = _read_geotiff_gpu(int_tif) assert gpu.attrs["nodata"] == eager.attrs["nodata"] np.testing.assert_array_equal( np.isnan(eager.data), np.isnan(gpu.data.get()), @@ -1944,7 +1944,7 @@ def test_eager(self, float_tif): def test_dask(self, float_tif): eager = open_geotiff(float_tif) - lazy = read_geotiff_dask(float_tif, chunks=2) + lazy = _read_geotiff_dask(float_tif, chunks=2) np.testing.assert_array_equal( np.isnan(eager.data), np.isnan(lazy.compute().data), ) @@ -1952,10 +1952,10 @@ def test_dask(self, float_tif): @_gpu_only def test_gpu(self, float_tif): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu eager = open_geotiff(float_tif) - gpu = read_geotiff_gpu(float_tif) + gpu = _read_geotiff_gpu(float_tif) np.testing.assert_array_equal( np.isnan(eager.data), np.isnan(gpu.data.get()), ) @@ -1971,7 +1971,7 @@ def test_eager(self, nan_tif): assert np.isnan(da.attrs["nodata"]) def test_dask_matches(self, nan_tif): - lazy = read_geotiff_dask(nan_tif, chunks=2) + lazy = _read_geotiff_dask(nan_tif, chunks=2) out = lazy.compute() # Source had no NaN pixels planted, so the float buffer carries # the original values. @@ -1989,7 +1989,7 @@ def test_eager_keeps_int_dtype_and_records_sentinel(self, oor_tif): def test_dask_matches(self, oor_tif): eager = open_geotiff(oor_tif) - lazy = read_geotiff_dask(oor_tif, chunks=2) + lazy = _read_geotiff_dask(oor_tif, chunks=2) assert lazy.dtype == eager.dtype np.testing.assert_array_equal( eager.data, lazy.compute().data, @@ -2043,7 +2043,7 @@ def test_dask_matches_eager(self, tmp_path): path = str(tmp_path / "miw_dask_2226.tif") self._build(path) eager = open_geotiff(path) - lazy = read_geotiff_dask(path, chunks=2) + lazy = _read_geotiff_dask(path, chunks=2) np.testing.assert_array_equal( np.isnan(eager.data), np.isnan(lazy.compute().data), ) @@ -2091,7 +2091,7 @@ def test_eager_keeps_literal_sentinel(self, int_tif): assert da.attrs["masked_nodata"] is False def test_dask_keeps_literal_sentinel(self, int_tif): - lazy = read_geotiff_dask(int_tif, chunks=2, mask_nodata=False) + lazy = _read_geotiff_dask(int_tif, chunks=2, mask_nodata=False) out = lazy.compute() assert out.dtype == np.uint8 assert int(out.data[0, 2]) == 255 @@ -2099,9 +2099,9 @@ def test_dask_keeps_literal_sentinel(self, int_tif): @_gpu_only def test_gpu_keeps_literal_sentinel(self, int_tif): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu - gpu = read_geotiff_gpu(int_tif, mask_nodata=False) + gpu = _read_geotiff_gpu(int_tif, mask_nodata=False) host = gpu.data.get() assert host.dtype == np.uint8 assert int(host[0, 2]) == 255 @@ -2115,7 +2115,7 @@ def test_eager_records_dtype_cast(self, int_tif): assert da.attrs.get("nodata_dtype_cast") == "float64" def test_dask_records_dtype_cast(self, int_tif): - lazy = read_geotiff_dask(int_tif, chunks=2, dtype="float64") + lazy = _read_geotiff_dask(int_tif, chunks=2, dtype="float64") assert lazy.dtype == np.float64 assert lazy.attrs.get("nodata_dtype_cast") == "float64" @@ -2158,12 +2158,12 @@ def simple_vrt(tmp_path): class TestVRTEagerParity: def test_vrt_masks_sentinel_to_nan(self, simple_vrt): - da = read_vrt(simple_vrt) + da = _read_vrt(simple_vrt) assert da.dtype.kind == "f" assert np.isnan(da.data[0, 2]) def test_vrt_mask_nodata_false_keeps_literal(self, simple_vrt): - da = read_vrt(simple_vrt, mask_nodata=False) + da = _read_vrt(simple_vrt, mask_nodata=False) # Literal -9999 survives in the float buffer. assert da.data[0, 2] == -9999.0 assert da.attrs.get("masked_nodata") is False @@ -2467,19 +2467,19 @@ def test_int_source_no_hit_keeps_sentinel(self, tmp_path): class TestDaskNumpy: - """``read_geotiff_dask`` (lazy dask + numpy backend).""" + """``_read_geotiff_dask`` (lazy dask + numpy backend).""" def test_float_source_with_sentinel(self, tmp_path): path = str(tmp_path / "tnss1988_dask_float_sentinel.tif") _write_float_tiff_1988(path, with_sentinel=True) - da = read_geotiff_dask(path, chunks=2) + da = _read_geotiff_dask(path, chunks=2) assert da.attrs["nodata"] == _SENTINEL_1988 assert da.attrs["masked_nodata"] is True def test_float_source_without_sentinel(self, tmp_path): path = str(tmp_path / "tnss1988_dask_float_no_sentinel.tif") _write_float_tiff_1988(path, with_sentinel=False) - da = read_geotiff_dask(path, chunks=2) + da = _read_geotiff_dask(path, chunks=2) assert "nodata" not in da.attrs assert "masked_nodata" not in da.attrs @@ -2494,7 +2494,7 @@ def test_int_source_with_in_range_sentinel(self, tmp_path): """ path = str(tmp_path / "tnss1988_dask_int_in_range.tif") _write_int_tiff_1988(path, with_sentinel_hit=False) - da = read_geotiff_dask(path, chunks=2) + da = _read_geotiff_dask(path, chunks=2) assert da.attrs["nodata"] == 65535 assert da.dtype.kind == "f" assert da.attrs["masked_nodata"] is True @@ -2510,7 +2510,7 @@ def test_int_source_with_out_of_range_sentinel(self, tmp_path): """ path = str(tmp_path / "tnss1988_dask_int_oor.tif") _build_uint16_with_out_of_range_nodata_1988(path) - da = read_geotiff_dask(path, chunks=2) + da = _read_geotiff_dask(path, chunks=2) assert da.attrs["nodata"] == -9999 assert da.dtype.kind == "u" assert da.attrs["masked_nodata"] is False @@ -2551,7 +2551,7 @@ def _build_vrt_1988(tmp_path, source_path, vrt_dtype, nodata_value, class TestVRTEager: - """``read_vrt`` (eager path) honours the split-attrs contract.""" + """``_read_vrt`` (eager path) honours the split-attrs contract.""" def test_float32_vrt_int_source_with_hit(self, tmp_path): """Float-typed VRT over int source with sentinel hit -> masked_nodata=True.""" @@ -2561,7 +2561,7 @@ def test_float32_vrt_int_source_with_hit(self, tmp_path): ) vrt = _build_vrt_1988(tmp_path, src, "Float32", 65535, filename="tnss1988_vrt_hit.vrt") - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.attrs["nodata"] == 65535.0 assert r.dtype.kind == "f" assert r.attrs["masked_nodata"] is True @@ -2580,7 +2580,7 @@ def test_uint16_vrt_int_source_no_hit(self, tmp_path): ) vrt = _build_vrt_1988(tmp_path, src, "UInt16", 65535, filename="tnss1988_vrt_nohit.vrt") - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.attrs["nodata"] == 65535.0 assert r.dtype.kind in ("u", "i") assert r.attrs["masked_nodata"] is False @@ -2606,13 +2606,13 @@ def test_vrt_no_nodata_emits_neither_attr(self, tmp_path): vrt = str(tmp_path / "tnss1988_vrt_no_nd.vrt") with open(vrt, "w") as f: f.write(vrt_xml) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert "nodata" not in r.attrs assert "masked_nodata" not in r.attrs class TestVRTChunked: - """``read_vrt(..., chunks=N)`` honours the split-attrs contract.""" + """``_read_vrt(..., chunks=N)`` honours the split-attrs contract.""" def test_chunked_int_source_in_range_sentinel(self, tmp_path): """Chunked VRT declares float64 for in-range int sentinel -> masked_nodata=True.""" @@ -2622,7 +2622,7 @@ def test_chunked_int_source_in_range_sentinel(self, tmp_path): ) vrt = _build_vrt_1988(tmp_path, src, "UInt16", 65535, filename="tnss1988_vrt_chunked.vrt") - r = read_vrt(vrt, chunks=2) + r = _read_vrt(vrt, chunks=2) assert r.attrs["nodata"] == 65535.0 # Chunked path promotes to float64 declared dtype. assert r.dtype == np.float64 @@ -2631,14 +2631,14 @@ def test_chunked_int_source_in_range_sentinel(self, tmp_path): @_gpu_only class TestGPU: - """``read_geotiff_gpu`` honours the split-attrs contract.""" + """``_read_geotiff_gpu`` honours the split-attrs contract.""" def test_int_source_with_hit(self, tmp_path): """Int source + sentinel hit on GPU -> masked_nodata=True (float).""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tnss1988_gpu_int_hit.tif") _write_int_tiff_1988(path, with_sentinel_hit=True) - da = read_geotiff_gpu(path) + da = _read_geotiff_gpu(path) assert da.attrs["nodata"] == 65535 assert np.dtype(str(da.dtype)).kind == "f" assert da.attrs["masked_nodata"] is True @@ -2649,20 +2649,20 @@ def test_int_source_no_hit_keeps_sentinel(self, tmp_path): Mirrors the eager-numpy contract: GPU masking only promotes int to float64 when at least one sentinel pixel is found. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tnss1988_gpu_int_nohit.tif") _write_int_tiff_1988(path, with_sentinel_hit=False) - da = read_geotiff_gpu(path) + da = _read_geotiff_gpu(path) assert da.attrs["nodata"] == 65535 assert np.dtype(str(da.dtype)).kind in ("u", "i") assert da.attrs["masked_nodata"] is False def test_dask_gpu_in_range_sentinel(self, tmp_path): """Dask+GPU declares float64 graph for in-range int sentinel.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = str(tmp_path / "tnss1988_gpu_dask_int.tif") _write_int_tiff_1988(path, with_sentinel_hit=False) - da = read_geotiff_gpu(path, chunks=2) + da = _read_geotiff_gpu(path, chunks=2) assert da.attrs["nodata"] == 65535 assert np.dtype(str(da.dtype)).kind == "f" assert da.attrs["masked_nodata"] is True @@ -3110,7 +3110,7 @@ def test_masked_nodata_false_preserves_nan_gpu(self, tmp_path): rasterio = pytest.importorskip("rasterio") import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu path = tmp_path / "test_1988_writer_gpu_unmasked.tif" arr_np = np.array( @@ -3130,7 +3130,7 @@ def test_masked_nodata_false_preserves_nan_gpu(self, tmp_path): "masked_nodata": False, }, ) - write_geotiff_gpu(da, str(path), compression="none") + _write_geotiff_gpu(da, str(path), compression="none") with rasterio.open(str(path)) as ds: on_disk = ds.read(1) @@ -3142,7 +3142,7 @@ def test_masked_nodata_true_restores_sentinel_gpu(self, tmp_path): rasterio = pytest.importorskip("rasterio") import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu path = tmp_path / "test_1988_writer_gpu_masked.tif" arr_np = np.array( @@ -3162,7 +3162,7 @@ def test_masked_nodata_true_restores_sentinel_gpu(self, tmp_path): "masked_nodata": True, }, ) - write_geotiff_gpu(da, str(path), compression="none") + _write_geotiff_gpu(da, str(path), compression="none") with rasterio.open(str(path)) as ds: on_disk = ds.read(1) @@ -3252,7 +3252,7 @@ def test_read_geotiff_dask_int_nodata_nan_rejected_by_default(tmp_path): """Dask path raises at graph-build time, before any chunk task fires.""" path = _build_uint16_tiff('nan', tmp_path) with pytest.raises(InvalidIntegerNodataError): - read_geotiff_dask(path, chunks=2) + _read_geotiff_dask(path, chunks=2) def test_read_geotiff_dask_int_nodata_fractional_rejected_by_default( @@ -3261,7 +3261,7 @@ def test_read_geotiff_dask_int_nodata_fractional_rejected_by_default( """Dask path raises at graph-build time for fractional int sentinels.""" path = _build_uint16_tiff('30.5', tmp_path) with pytest.raises(InvalidIntegerNodataError): - read_geotiff_dask(path, chunks=2) + _read_geotiff_dask(path, chunks=2) # ---------------------------------------------------------------------- @@ -3325,7 +3325,7 @@ def test_open_geotiff_opt_in_restores_noop_eager(tmp_path, nodata_str): def test_read_geotiff_dask_opt_in_restores_noop(tmp_path, nodata_str): """``allow_invalid_nodata=True`` keeps the pre-2441 no-op for dask.""" path = _build_uint16_tiff(nodata_str, tmp_path) - da = read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) + da = _read_geotiff_dask(path, chunks=2, allow_invalid_nodata=True) assert da.dtype == np.uint16 np.testing.assert_array_equal(da.compute().values, [[10, 20], [30, 40]]) @@ -3338,11 +3338,11 @@ def test_read_geotiff_dask_opt_in_restores_noop(tmp_path, nodata_str): @_gpu_only def test_read_geotiff_gpu_int_nodata_nan_rejected_by_default(tmp_path): """GPU read entry point raises before kicking off the device decode.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _build_uint16_tiff('nan', tmp_path) with pytest.raises(InvalidIntegerNodataError): - read_geotiff_gpu(path) + _read_geotiff_gpu(path) @_gpu_only @@ -3350,10 +3350,10 @@ def test_read_geotiff_gpu_int_nodata_opt_in_restores_noop(tmp_path): """GPU opt-in keeps the no-op (sentinel cannot match any uint16 pixel).""" import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _build_uint16_tiff('nan', tmp_path) - da = read_geotiff_gpu(path, allow_invalid_nodata=True) + da = _read_geotiff_gpu(path, allow_invalid_nodata=True) # Buffer stays uint16 on the device. assert da.dtype == cupy.uint16 arr = da.data.get() @@ -3365,8 +3365,8 @@ def test_read_geotiff_gpu_chunked_int_nodata_rejected_by_default(tmp_path): """dask+cupy backend rejects at metadata parse, before any chunk task is scheduled. Closes the four-backend matrix explicitly. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path = _build_uint16_tiff('nan', tmp_path) with pytest.raises(InvalidIntegerNodataError): - read_geotiff_gpu(path, chunks=2) + _read_geotiff_gpu(path, chunks=2) diff --git a/xrspatial/geotiff/tests/read/test_overview.py b/xrspatial/geotiff/tests/read/test_overview.py index dd531cea4..73d436bbd 100644 --- a/xrspatial/geotiff/tests/read/test_overview.py +++ b/xrspatial/geotiff/tests/read/test_overview.py @@ -10,7 +10,7 @@ the reduction factor, across all four backends. * ``overview_level`` type checks fire up front (and before unrelated source / chunk / GPU-policy errors) on ``open_geotiff``, - ``read_geotiff_dask``, and ``read_geotiff_gpu``. + ``_read_geotiff_dask``, and ``_read_geotiff_gpu``. """ from __future__ import annotations @@ -679,7 +679,7 @@ def test_overview_level_typeerror_names_value(cog_with_overview_2074): # ========================================================================= # # ``open_geotiff`` has an up-front guard. The direct backends -# (``read_geotiff_dask``, ``read_geotiff_gpu``) reach the same selector +# (``_read_geotiff_dask``, ``_read_geotiff_gpu``) reach the same selector # but only after source coercion, chunk validation, and (on the GPU path) # ``on_gpu_failure`` resolution. This section mirrors the type-validation # tests against the two direct backends and asserts ordering: the @@ -710,84 +710,84 @@ def cog_with_overview_2160(tmp_path): # --------------------------------------------------------------------------- -# read_geotiff_dask +# _read_geotiff_dask # --------------------------------------------------------------------------- @pytest.mark.parametrize("value", [True, False]) def test_dask_overview_level_bool_raises_typeerror(cog_with_overview_2160, value): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_dask(path, overview_level=value) + _read_geotiff_dask(path, overview_level=value) def test_dask_overview_level_str_raises_typeerror(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="str"): - read_geotiff_dask(path, overview_level="0") + _read_geotiff_dask(path, overview_level="0") def test_dask_overview_level_float_raises_typeerror(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="float"): - read_geotiff_dask(path, overview_level=1.0) + _read_geotiff_dask(path, overview_level=1.0) def test_dask_overview_level_zero_succeeds(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = cog_with_overview_2160 - result = read_geotiff_dask(path, overview_level=0) + result = _read_geotiff_dask(path, overview_level=0) assert result.shape == arr.shape def test_dask_overview_level_one_succeeds(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = cog_with_overview_2160 - result = read_geotiff_dask(path, overview_level=1) + result = _read_geotiff_dask(path, overview_level=1) assert result.shape == (arr.shape[0] // 2, arr.shape[1] // 2) def test_dask_overview_level_none_succeeds(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = cog_with_overview_2160 - result = read_geotiff_dask(path, overview_level=None) + result = _read_geotiff_dask(path, overview_level=None) assert result.shape == arr.shape @pytest.mark.parametrize("value", [np.int64(0), np.int32(0)]) def test_dask_overview_level_numpy_int_zero_succeeds(cog_with_overview_2160, value): """``np.int64`` / ``np.int32`` should be accepted like Python ints.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = cog_with_overview_2160 - result = read_geotiff_dask(path, overview_level=value) + result = _read_geotiff_dask(path, overview_level=value) assert result.shape == arr.shape @pytest.mark.parametrize("value", [np.int64(1), np.int32(1)]) def test_dask_overview_level_numpy_int_one_succeeds(cog_with_overview_2160, value): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = cog_with_overview_2160 - result = read_geotiff_dask(path, overview_level=value) + result = _read_geotiff_dask(path, overview_level=value) assert result.shape == (arr.shape[0] // 2, arr.shape[1] // 2) def test_dask_overview_level_typeerror_names_value(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, _ = cog_with_overview_2160 with pytest.raises(TypeError) as exc_info: - read_geotiff_dask(path, overview_level="not-an-int") + _read_geotiff_dask(path, overview_level="not-an-int") msg = str(exc_info.value) assert "str" in msg assert "not-an-int" in msg @@ -799,28 +799,28 @@ def test_dask_overview_level_typeerror_names_value(cog_with_overview_2160): def test_dask_overview_level_check_runs_before_source_coercion(): """Bad source + bad overview_level should report overview_level first.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask with pytest.raises(TypeError, match="bool"): - read_geotiff_dask("/nonexistent/path-2160.tif", overview_level=True) + _read_geotiff_dask("/nonexistent/path-2160.tif", overview_level=True) def test_dask_overview_level_check_runs_before_chunks_validation( cog_with_overview_2160): """Bad chunks + bad overview_level should report overview_level first.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_dask(path, chunks=0, overview_level=True) + _read_geotiff_dask(path, chunks=0, overview_level=True) # --------------------------------------------------------------------------- -# read_geotiff_gpu +# _read_geotiff_gpu # --------------------------------------------------------------------------- # # These tests must not import cupy. The validator runs at the top of -# ``read_geotiff_gpu`` before the cupy import, so the bad-input cases +# ``_read_geotiff_gpu`` before the cupy import, so the bad-input cases # raise ``TypeError`` on a CPU-only machine. The "succeeds" cases that # actually need a GPU stay gated on cupy via ``importorskip``. @@ -828,35 +828,35 @@ def test_dask_overview_level_check_runs_before_chunks_validation( @pytest.mark.parametrize("value", [True, False]) def test_gpu_overview_level_bool_raises_typeerror_no_cupy( cog_with_overview_2160, value): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu(path, overview_level=value) + _read_geotiff_gpu(path, overview_level=value) def test_gpu_overview_level_str_raises_typeerror_no_cupy(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="str"): - read_geotiff_gpu(path, overview_level="0") + _read_geotiff_gpu(path, overview_level="0") def test_gpu_overview_level_float_raises_typeerror_no_cupy(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="float"): - read_geotiff_gpu(path, overview_level=1.0) + _read_geotiff_gpu(path, overview_level=1.0) def test_gpu_overview_level_typeerror_names_value_no_cupy(cog_with_overview_2160): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError) as exc_info: - read_geotiff_gpu(path, overview_level="not-an-int") + _read_geotiff_gpu(path, overview_level="not-an-int") msg = str(exc_info.value) assert "str" in msg assert "not-an-int" in msg @@ -864,20 +864,20 @@ def test_gpu_overview_level_typeerror_names_value_no_cupy(cog_with_overview_2160 def test_gpu_overview_level_check_runs_before_source_coercion(): """Bad source + bad overview_level should report overview_level first.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu("/nonexistent/path-2160.tif", overview_level=True) + _read_geotiff_gpu("/nonexistent/path-2160.tif", overview_level=True) def test_gpu_overview_level_check_runs_before_chunks_validation( cog_with_overview_2160): """Bad chunks + bad overview_level should report overview_level first.""" - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu(path, chunks=0, overview_level=True) + _read_geotiff_gpu(path, chunks=0, overview_level=True) def test_gpu_overview_level_check_runs_before_on_gpu_failure_validation( @@ -890,26 +890,26 @@ def test_gpu_overview_level_check_runs_before_on_gpu_failure_validation( ``on_gpu_failure`` and a bad ``overview_level`` would get the ValueError from the policy check, masking the real type bug. """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu(path, on_gpu_failure="bogus", overview_level=True) + _read_geotiff_gpu(path, on_gpu_failure="bogus", overview_level=True) def test_gpu_overview_level_check_runs_before_chunked_dispatch( cog_with_overview_2160): """``chunks=`` routes through ``_read_geotiff_gpu_chunked``; the - validator at the top of ``read_geotiff_gpu`` must fire before that + validator at the top of ``_read_geotiff_gpu`` must fire before that branch, otherwise a bad ``overview_level`` would only surface via - the inner ``read_geotiff_dask`` call (which now also validates, + the inner ``_read_geotiff_dask`` call (which now also validates, but the contract is that the outer entry point reports it first). """ - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu path, _ = cog_with_overview_2160 with pytest.raises(TypeError, match="bool"): - read_geotiff_gpu(path, chunks=32, overview_level=True) + _read_geotiff_gpu(path, chunks=32, overview_level=True) # =========================================================================== # ngjit 2x2 overview kernels parity (#2413) diff --git a/xrspatial/geotiff/tests/read/test_tiling.py b/xrspatial/geotiff/tests/read/test_tiling.py index 8e89dafd8..158a86793 100644 --- a/xrspatial/geotiff/tests/read/test_tiling.py +++ b/xrspatial/geotiff/tests/read/test_tiling.py @@ -34,7 +34,7 @@ from xrspatial.geotiff import _decode as _decode_mod from xrspatial.geotiff import _reader as _reader_mod -from xrspatial.geotiff import open_geotiff, read_geotiff_gpu, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_geotiff_gpu, to_geotiff from xrspatial.geotiff._compression import COMPRESSION_NONE, unpack_bits from xrspatial.geotiff._dtypes import (resolve_bits_per_sample, resolve_sample_format, tiff_dtype_to_numpy) @@ -193,7 +193,7 @@ def test_huge_tile_byte_count_rejected(self, tmp_path, monkeypatch): monkeypatch.setenv("XRSPATIAL_COG_MAX_TILE_BYTES", str(1024 * 1024)) with pytest.raises(ValueError, match="TileByteCount"): - read_geotiff_gpu(path) + _read_geotiff_gpu(path) @_gpu_only def test_error_message_names_value_and_cap(self, tmp_path, monkeypatch): @@ -202,7 +202,7 @@ def test_error_message_names_value_and_cap(self, tmp_path, monkeypatch): monkeypatch.setenv("XRSPATIAL_COG_MAX_TILE_BYTES", str(1024)) with pytest.raises(ValueError) as excinfo: - read_geotiff_gpu(path) + _read_geotiff_gpu(path) msg = str(excinfo.value) assert "52,428,800" in msg or "52428800" in msg assert "1,024" in msg or "1024" in msg @@ -216,7 +216,7 @@ def test_normal_gpu_read_under_default_cap(self, tmp_path): path = str(tmp_path / "normal_gpu.tif") to_geotiff(da, path, tile_size=32, compression="deflate") - result = read_geotiff_gpu(path) + result = _read_geotiff_gpu(path) np.testing.assert_array_equal(result.data.get(), arr) @_gpu_only @@ -228,7 +228,7 @@ def test_env_override_lifts_cap(self, tmp_path, monkeypatch): "XRSPATIAL_COG_MAX_TILE_BYTES", str(64 * 1024 * 1024)) try: - read_geotiff_gpu(path) + _read_geotiff_gpu(path) except Exception as exc: assert "exceeds the per-tile safety cap" not in str(exc), ( "cap loop fired despite the env override lifting the cap" @@ -246,7 +246,7 @@ def test_chunked_huge_tile_byte_count_rejected( "XRSPATIAL_COG_MAX_TILE_BYTES", str(1024 * 1024)) with pytest.raises(ValueError, match="TileByteCount"): - read_geotiff_gpu(path, chunks=32) + _read_geotiff_gpu(path, chunks=32) # --------------------------------------------------------------------------- @@ -1121,7 +1121,7 @@ def test_planar_multiband_gpu_matches_cpu( path = os.path.join(str(tmp_path), "planar_gpu_2429.tif") _write_planar_matrix_tiff(path, data, planar=planar, tiled=tiled) cpu = np.asarray(open_geotiff(path).data) - gpu_da = read_geotiff_gpu(path) + gpu_da = _read_geotiff_gpu(path) assert gpu_da.dims == ("y", "x", "band") assert gpu_da.shape == (height, width, bands) @@ -1165,7 +1165,7 @@ def test_planar_singleband_gpu(tiled, tmp_path): if tiled: kwargs["tile"] = (32, 32) tifffile.imwrite(path, data, **kwargs) - out = read_geotiff_gpu(path) + out = _read_geotiff_gpu(path) assert out.dims == ("y", "x") assert out.shape == (48, 80) np.testing.assert_array_equal(out.data.get(), data) diff --git a/xrspatial/geotiff/tests/release_gates/test_features.py b/xrspatial/geotiff/tests/release_gates/test_features.py index 5064125ed..a734238c6 100644 --- a/xrspatial/geotiff/tests/release_gates/test_features.py +++ b/xrspatial/geotiff/tests/release_gates/test_features.py @@ -14,7 +14,7 @@ documented promotions and demotions stay pinned). * Tier-aware codec gate on the writer (Tier 3 ``allow_experimental_codecs``; Tier 4 ``allow_internal_only_jpeg``); - ``to_geotiff`` and ``write_geotiff_gpu`` signature pins. + ``to_geotiff`` and ``_write_geotiff_gpu`` signature pins. * Typed-error refusals at the VRT parser and the eager writer for unsupported feature combinations (warped VRTs, derived raster bands, kernel-filtered sources, mixed per-source nodata, rotated transforms, @@ -49,8 +49,8 @@ from xrspatial.geotiff import (SUPPORTED_FEATURES, GeoTIFFAmbiguousMetadataError, GeoTIFFFallbackWarning, RotatedTransformError, UnsupportedGeoTIFFFeatureError, VRTStableSourcesOnlyError, - open_geotiff, read_geotiff_dask, read_vrt, to_geotiff, - write_geotiff_gpu) + open_geotiff, _read_geotiff_dask, _read_vrt, to_geotiff, + _write_geotiff_gpu) from xrspatial.geotiff._attrs import _VALID_COMPRESSIONS from xrspatial.geotiff._compression import (packbits_compress, packbits_decompress, zstd_compress, zstd_decompress) @@ -619,8 +619,8 @@ def test_float16_auto_promotion(self, tmp_path): np.testing.assert_array_almost_equal(result.values, 3.14, decimal=2) def test_vrt_write_and_read_back(self, tmp_path): - """write_vrt generates a valid VRT that reads back correctly.""" - from xrspatial.geotiff import write_vrt + """build_vrt generates a valid VRT that reads back correctly.""" + from xrspatial.geotiff import build_vrt from xrspatial.geotiff._geotags import GeoTransform # Write two tiles with known geo transforms @@ -638,7 +638,7 @@ def test_vrt_write_and_read_back(self, tmp_path): write(right, rpath, geo_transform=gt_right, compression='none', tiled=False) vrt_path = str(tmp_path / 'mosaic.vrt') - write_vrt(vrt_path, [lpath, rpath]) + build_vrt(vrt_path, [lpath, rpath]) da = open_geotiff(vrt_path) assert da.shape == (4, 8) @@ -646,8 +646,8 @@ def test_vrt_write_and_read_back(self, tmp_path): np.testing.assert_array_equal(da.values[:, 4:], right) def test_dask_vrt(self, tmp_path): - """read_geotiff_dask handles VRT files.""" - from xrspatial.geotiff import read_geotiff_dask + """_read_geotiff_dask handles VRT files.""" + from xrspatial.geotiff import _read_geotiff_dask arr = np.arange(16, dtype=np.float32).reshape(4, 4) tile_path = str(tmp_path / 'tile.tif') @@ -670,7 +670,7 @@ def test_dask_vrt(self, tmp_path): f.write(vrt_xml) import dask.array as da - result = read_geotiff_dask(vrt_path, chunks=2) + result = _read_geotiff_dask(vrt_path, chunks=2) assert isinstance(result.data, da.Array) computed = result.compute() np.testing.assert_array_equal(computed.values, arr) @@ -842,7 +842,7 @@ def test_vrt_nodata(self, tmp_path): def test_read_vrt_function(self, tmp_path): """read_vrt() works directly.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt arr = np.arange(16, dtype=np.float32).reshape(4, 4) tile_path = self._write_tile(tmp_path, 'tile.tif', arr) @@ -852,7 +852,7 @@ def test_read_vrt_function(self, tmp_path): width=4, height=4, ) - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) assert da.name == 'mosaic' np.testing.assert_array_equal(da.values, arr) @@ -950,7 +950,7 @@ def test_vrt_float64_fractional_nodata_masked(self, tmp_path): def test_vrt_pixel_is_point_no_half_pixel_shift(self, tmp_path): """VRT with AREA_OR_POINT=Point does not apply a half-pixel shift. - Before the fix, ``read_vrt`` always added ``(c + 0.5) * res`` + Before the fix, ``_read_vrt`` always added ``(c + 0.5) * res`` to the GeoTransform origin, even when the VRT advertised Point registration. That shifted coords by half a cell in world space on any Point-tagged VRT. @@ -1110,7 +1110,7 @@ def test_memory_filesystem_full_roundtrip(self, tmp_path): fs.rm('/roundtrip.tif') def test_dask_path_fsspec_uri_1749(self, tmp_path): - """read_geotiff_dask supports fsspec URIs. + """_read_geotiff_dask supports fsspec URIs. The eager path already routed through _CloudSource via _read_to_array. The dask path's _read_geo_info used plain @@ -2741,16 +2741,16 @@ def test_planar_via_public_api(self, tmp_path): class TestDaskReads: def test_dask_basic(self, tmp_path): - """read_geotiff_dask returns a dask-backed DataArray.""" + """_read_geotiff_dask returns a dask-backed DataArray.""" import dask.array as da - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask arr = np.arange(256, dtype=np.float32).reshape(16, 16) path = str(tmp_path / 'dask_test.tif') write(arr, path, compression='none', tiled=False) - result = read_geotiff_dask(path, chunks=8) + result = _read_geotiff_dask(path, chunks=8) assert isinstance(result.data, da.Array) assert result.shape == (16, 16) @@ -2760,7 +2760,7 @@ def test_dask_basic(self, tmp_path): def test_dask_coords(self, tmp_path): """Dask read preserves coordinates and CRS.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask from xrspatial.geotiff._geotags import GeoTransform arr = np.ones((8, 8), dtype=np.float32) @@ -2769,21 +2769,21 @@ def test_dask_coords(self, tmp_path): write(arr, path, geo_transform=gt, crs_epsg=4326, compression='none', tiled=False) - result = read_geotiff_dask(path, chunks=4) + result = _read_geotiff_dask(path, chunks=4) assert result.attrs['crs'] == 4326 assert len(result.coords['y']) == 8 assert len(result.coords['x']) == 8 def test_dask_nodata(self, tmp_path): """Nodata masking applied per-chunk.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask arr = np.array([[1.0, -9999.0], [-9999.0, 2.0], [3.0, 4.0], [5.0, -9999.0]], dtype=np.float32) path = str(tmp_path / 'dask_nodata.tif') write(arr, path, compression='none', tiled=False, nodata=-9999.0) - result = read_geotiff_dask(path, chunks=2) + result = _read_geotiff_dask(path, chunks=2) computed = result.compute() assert np.isnan(computed.values[0, 1]) assert np.isnan(computed.values[1, 0]) @@ -2791,13 +2791,13 @@ def test_dask_nodata(self, tmp_path): def test_dask_chunk_tuple(self, tmp_path): """Chunks as (row, col) tuple.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask arr = np.arange(200, dtype=np.float32).reshape(10, 20) path = str(tmp_path / 'dask_tuple.tif') write(arr, path, compression='deflate', tiled=False) - result = read_geotiff_dask(path, chunks=(5, 10)) + result = _read_geotiff_dask(path, chunks=(5, 10)) computed = result.compute() np.testing.assert_array_equal(computed.values, arr) @@ -2870,13 +2870,14 @@ def test_all_lists_supported_functions(self): # Tiered feature inventory exposed alongside the writer's # ``allow_experimental_codecs`` opt-in. 'SUPPORTED_FEATURES', + # Read/write surface consolidated on the two dispatchers + # (open_geotiff / to_geotiff) plus the VRT-mosaic builder. + # The backend functions (_read_geotiff_gpu, _read_geotiff_dask, + # _read_vrt, _write_geotiff_gpu) are private; the dispatchers + # route to them from their kwargs. + 'build_vrt', 'open_geotiff', - 'read_geotiff_gpu', - 'read_geotiff_dask', - 'read_vrt', 'to_geotiff', - 'write_geotiff_gpu', - 'write_vrt', } assert set(g.__all__) == expected @@ -3140,11 +3141,11 @@ def test_to_geotiff_signature_has_allow_experimental_codecs(): def test_write_geotiff_gpu_signature_has_allow_experimental_codecs(): - """``write_geotiff_gpu`` carries the same kwarg with the same + """``_write_geotiff_gpu`` carries the same kwarg with the same default, so the two writers expose a consistent surface and the auto-dispatch path forwards a single value to either. """ - params = inspect.signature(write_geotiff_gpu).parameters + params = inspect.signature(_write_geotiff_gpu).parameters assert 'allow_experimental_codecs' in params assert params['allow_experimental_codecs'].default is False @@ -3223,7 +3224,7 @@ def test_experimental_codec_opt_in_emits_warning(tmp_path, codec): "the codec carries no cross-backend parity claim.") # Exactly one warning per call. Pinning the count catches the # double-warn regression where the CPU dispatcher fires the - # warning and then ``write_geotiff_gpu`` fires it again on the GPU + # warning and then ``_write_geotiff_gpu`` fires it again on the GPU # dispatch path; the CPU dispatcher gates its warning on # ``not use_gpu`` to keep this invariant on the GPU path too. assert len(fallback) == 1, ( @@ -3298,7 +3299,7 @@ def test_pansharpened_vrt_subclass_rejected_at_parse(): The subClass check covers every GDAL VRT subclass uniformly, not just the warped one. Pin the pansharpened case so a caller who - points read_vrt at a pansharpened VRT sees the same actionable + points _read_vrt at a pansharpened VRT sees the same actionable failure rather than silently mis-reading. """ xml = ( @@ -3313,7 +3314,7 @@ def test_derived_rasterband_subclass_rejected_at_parse(): """A ```` is rejected. Derived raster bands declare a pixel-function expression evaluated - over the sources. read_vrt has no pixel-function evaluator and + over the sources. _read_vrt has no pixel-function evaluator and would drop straight to the simple-source path, producing wrong output. Pin the typed error and the band number in the message. """ @@ -3415,7 +3416,7 @@ def test_dataset_level_gcplist_rejected_at_parse(): """A dataset-level ```` (ground-control points) is rejected. GCPList signals a non-axis-aligned georeferencing model that - read_vrt cannot honour. Pin the rejection so a future refactor + _read_vrt cannot honour. Pin the rejection so a future refactor cannot regress to the silent no-op pre-#2349 behaviour. """ xml = ( @@ -3432,7 +3433,7 @@ def test_overview_list_band_child_still_passes(tmp_path): """```` and ```` band children are informational. GDAL emits these on VRTs whose source GeoTIFFs carry external - overviews. read_vrt does not consume VRT-level overview + overviews. _read_vrt does not consume VRT-level overview declarations (the source-side reader handles overviews via ``overview_level=``), so the elements were and remain no-ops. Pin the allow-list so the catch-all "unknown element" @@ -3663,7 +3664,7 @@ def test_vrt_with_skewed_geotransform_rejected(tmp_path): The GDAL GeoTransform skew terms (positions 2 and 4 in the GDAL ordering) flag a warped / reprojection VRT or a rotated - source. read_vrt has no resampler for the warped case; pin the + source. _read_vrt has no resampler for the warped case; pin the existing typed error so a future refactor cannot regress to the silent no-georef fallback. """ @@ -3765,17 +3766,17 @@ def test_open_geotiff_vrt_stable_only_with_experimental_unlock(tmp_path): def test_read_vrt_stable_only_rejected_by_default(tmp_path): - """Direct ``read_vrt(stable_only=True)`` raises the typed error too.""" + """Direct ``_read_vrt(stable_only=True)`` raises the typed error too.""" path = _write_minimal_vrt(tmp_path, "read_vrt_direct") with pytest.raises(VRTStableSourcesOnlyError): - read_vrt(path, stable_only=True) + _read_vrt(path, stable_only=True) def test_read_geotiff_dask_vrt_stable_only_rejected(tmp_path): - """``read_geotiff_dask`` forwards the kwarg to ``read_vrt`` for VRT sources.""" + """``_read_geotiff_dask`` forwards the kwarg to ``_read_vrt`` for VRT sources.""" path = _write_minimal_vrt(tmp_path, "read_dask_vrt") with pytest.raises(VRTStableSourcesOnlyError): - read_geotiff_dask(path, stable_only=True) + _read_geotiff_dask(path, stable_only=True) def test_vrt_stable_only_error_is_geotiff_ambiguous_metadata_error(): @@ -3798,4 +3799,4 @@ def test_read_vrt_stable_only_no_op_on_default(tmp_path): """ path = _write_minimal_vrt(tmp_path, "read_vrt_default") with pytest.raises(VRTUnsupportedError): - read_vrt(path) + _read_vrt(path) diff --git a/xrspatial/geotiff/tests/release_gates/test_stable_features.py b/xrspatial/geotiff/tests/release_gates/test_stable_features.py index 99a4aa1e6..1122529b5 100644 --- a/xrspatial/geotiff/tests/release_gates/test_stable_features.py +++ b/xrspatial/geotiff/tests/release_gates/test_stable_features.py @@ -74,7 +74,7 @@ ) from xrspatial.geotiff import (SUPPORTED_FEATURES, UnsafeURLError, open_geotiff, # noqa: E402 - read_geotiff_dask, to_geotiff) + _read_geotiff_dask, to_geotiff) from xrspatial.geotiff._compression import (COMPRESSION_DEFLATE, COMPRESSION_LZW, # noqa: E402 COMPRESSION_NONE, COMPRESSION_PACKBITS, COMPRESSION_ZSTD) @@ -870,7 +870,7 @@ def test_release_gate_dask_read_is_lazy(tmp_path) -> None: # disagree between the eager and dask paths is the highest release risk # for the GeoTIFF surface. This block reads each fixture in a small # representative corpus once through ``open_geotiff`` and once through -# ``read_geotiff_dask``, then asserts full raster equivalence. +# ``_read_geotiff_dask``, then asserts full raster equivalence. # Corpus fixtures live under ``golden_corpus/fixtures``. _EAGER_DASK_FIXTURES_DIR = ( @@ -1024,7 +1024,7 @@ def test_release_gate_eager_dask_full_parity( ) eager = open_geotiff(str(path), **open_kwargs) - lazy = read_geotiff_dask( + lazy = _read_geotiff_dask( str(path), chunks=_EAGER_DASK_CHUNK_SIZE, **open_kwargs, ) @@ -1556,9 +1556,9 @@ def _overview_read_levels_eager(path: str) -> dict: def _overview_read_levels_dask(path: str) -> dict: - out = {0: read_geotiff_dask(path, chunks=8)} + out = {0: _read_geotiff_dask(path, chunks=8)} for i, _ in enumerate(_OVERVIEW_FACTORS, start=1): - out[i] = read_geotiff_dask(path, chunks=8, overview_level=i) + out[i] = _read_geotiff_dask(path, chunks=8, overview_level=i) return out @@ -1904,7 +1904,7 @@ def _wsp_open_eager(path, *, window=None): def _wsp_open_dask(path, *, window=None): - return read_geotiff_dask(str(path), window=window, chunks=32) + return _read_geotiff_dask(str(path), window=window, chunks=32) _WSP_READERS = ( @@ -2367,7 +2367,7 @@ def test_release_gate_negative_rotated_dask( ) -> None: """Dask path raises the same typed error, uniformly with the eager path.""" with pytest.raises(RotatedTransformError) as excinfo: - read_geotiff_dask(_neg_rotated_geotiff_path, chunks=2) + _read_geotiff_dask(_neg_rotated_geotiff_path, chunks=2) _neg_assert_rotated_message(str(excinfo.value)) diff --git a/xrspatial/geotiff/tests/test_polish.py b/xrspatial/geotiff/tests/test_polish.py index a906587c0..723635e26 100644 --- a/xrspatial/geotiff/tests/test_polish.py +++ b/xrspatial/geotiff/tests/test_polish.py @@ -3,9 +3,9 @@ Covers: * early ``compression`` validation in ``to_geotiff`` -* read dispatch leaves ``read_geotiff_dask`` with a defensive - ``.vrt`` fallback that delegates to ``read_vrt`` -* ``write_vrt`` docstring lists kwargs (rejects unknown ones) +* read dispatch leaves ``_read_geotiff_dask`` with a defensive + ``.vrt`` fallback that delegates to ``_read_vrt`` +* ``build_vrt`` docstring lists kwargs (rejects unknown ones) * predictor doc covers True/2 equivalence and 3=fp * ``tile_size`` warns when ``tiled=False`` and non-default * mmap cache eviction (LRU + env var override) @@ -23,7 +23,7 @@ import numpy as np import pytest -from xrspatial.geotiff import read_geotiff_dask, to_geotiff, write_vrt +from xrspatial.geotiff import _read_geotiff_dask, to_geotiff, build_vrt from xrspatial.geotiff._reader import _MmapCache, read_to_array from xrspatial.geotiff._writer import _MAX_OVERVIEW_LEVELS, write @@ -61,9 +61,9 @@ def test_validation_runs_before_vrt_dispatch(self, tmp_path): class TestC2ReadDispatch: def test_read_geotiff_dask_handles_vrt_directly(self, tmp_path): - # Build a 2-tile VRT and confirm read_geotiff_dask routes to the + # Build a 2-tile VRT and confirm _read_geotiff_dask routes to the # VRT reader without trying to parse XML as TIFF. - from xrspatial.geotiff import write_vrt as wv + from xrspatial.geotiff import build_vrt as wv arr = np.arange(64, dtype=np.float32).reshape(8, 8) a_path = str(tmp_path / 'a_1488.tif') b_path = str(tmp_path / 'b_1488.tif') @@ -75,12 +75,12 @@ def test_read_geotiff_dask_handles_vrt_directly(self, tmp_path): vrt_path = str(tmp_path / 'mosaic_1488.vrt') wv(vrt_path, [a_path, b_path]) - result = read_geotiff_dask(vrt_path, chunks=8) + result = _read_geotiff_dask(vrt_path, chunks=8) assert result.dims == ('y', 'x') # --------------------------------------------------------------------------- -# write_vrt kwargs documented +# build_vrt kwargs documented # --------------------------------------------------------------------------- class TestC5WriteVrtKwargs: @@ -93,7 +93,7 @@ def test_known_kwargs_accepted(self, tmp_path): # canonical name (``crs_wkt`` is the deprecated alias); pass # ``crs=None`` instead of the deprecated alias to avoid the # DeprecationWarning the alias now emits. - write_vrt(vrt_path, [a_path], relative=False, crs=None, + build_vrt(vrt_path, [a_path], relative=False, crs=None, nodata=-9999.0) assert os.path.exists(vrt_path) @@ -103,14 +103,14 @@ def test_unknown_kwarg_raises_typeerror(self, tmp_path): write(arr, a_path, compression='none') vrt_path = str(tmp_path / 'mosaic_c5b_1488.vrt') with pytest.raises(TypeError): - write_vrt(vrt_path, [a_path], not_a_real_kwarg=True) + build_vrt(vrt_path, [a_path], not_a_real_kwarg=True) def test_docstring_lists_kwargs(self): # Defensive: the docstring is the contract here -- guard # against future regressions. - assert 'relative' in write_vrt.__doc__ - assert 'crs_wkt' in write_vrt.__doc__ - assert 'nodata' in write_vrt.__doc__ + assert 'relative' in build_vrt.__doc__ + assert 'crs_wkt' in build_vrt.__doc__ + assert 'nodata' in build_vrt.__doc__ # --------------------------------------------------------------------------- @@ -374,13 +374,13 @@ def test_excessive_chunks_raises_with_hint(self, tmp_path): path = str(tmp_path / 'p5_1488.tif') write(arr, path, compression='none', tiled=True, tile_size=64) with pytest.raises(ValueError, match=r"50,000-task cap"): - read_geotiff_dask(path, chunks=2) + _read_geotiff_dask(path, chunks=2) def test_normal_chunks_pass(self, tmp_path): arr = np.zeros((512, 512), dtype=np.uint8) path = str(tmp_path / 'p5b_1488.tif') write(arr, path, compression='none', tiled=True, tile_size=128) - result = read_geotiff_dask(path, chunks=128) + result = _read_geotiff_dask(path, chunks=128) assert result.shape == (512, 512) diff --git a/xrspatial/geotiff/tests/test_round_trip.py b/xrspatial/geotiff/tests/test_round_trip.py index 191ca4965..81dffa647 100644 --- a/xrspatial/geotiff/tests/test_round_trip.py +++ b/xrspatial/geotiff/tests/test_round_trip.py @@ -56,7 +56,7 @@ from hypothesis import HealthCheck, assume, event, given, settings from hypothesis import strategies as st -from xrspatial.geotiff import open_geotiff, to_geotiff, write_vrt +from xrspatial.geotiff import open_geotiff, to_geotiff, build_vrt from xrspatial.geotiff._geotags import _NO_GEOREF_KEY, GeoTransform from xrspatial.geotiff._writer import write @@ -631,7 +631,7 @@ class TestVRTRoundTripFromCorpus: in-memory array back as a plain GeoTIFF (no VRT) and asserts the re-read matches the original VRT read byte-for-byte. The VRT XML itself does not round-trip -- the writer emits a single TIFF, not - a VRT pointing at sources. Use ``write_vrt`` explicitly when a VRT + a VRT pointing at sources. Use ``build_vrt`` explicitly when a VRT is the desired output. """ @@ -662,7 +662,7 @@ def test_vrt_mosaic_round_trips_as_geotiff(self, tmp_path, source_name): dst.write(data, 1) vrt = tmp_path / "vrt_mosaic_1986.vrt" - write_vrt(str(vrt), [str(left), str(right)]) + build_vrt(str(vrt), [str(left), str(right)]) da1 = open_geotiff(str(vrt)) expected = np.concatenate([data, data], axis=1) diff --git a/xrspatial/geotiff/tests/test_stable_only_remote_2821.py b/xrspatial/geotiff/tests/test_stable_only_remote_2821.py index 09eb645fd..e66856a3c 100644 --- a/xrspatial/geotiff/tests/test_stable_only_remote_2821.py +++ b/xrspatial/geotiff/tests/test_stable_only_remote_2821.py @@ -6,7 +6,7 @@ stable-only sources via ``stable_only=True`` must not be served from a remote source. Before the fix the gate only fired on the VRT dispatch: the eager non-VRT path called ``read_to_array`` directly and the dask -path only forwarded ``stable_only`` to ``read_vrt`` for ``.vrt`` paths, +path only forwarded ``stable_only`` to ``_read_vrt`` for ``.vrt`` paths, so an HTTP / fsspec source slipped through unchecked. The tests push a small valid (stable-codec, local-style) GeoTIFF into @@ -22,7 +22,7 @@ import pytest from xrspatial.geotiff import (GeoTIFFAmbiguousMetadataError, RemoteStableSourcesOnlyError, - open_geotiff, read_geotiff_dask, read_geotiff_gpu) + open_geotiff, _read_geotiff_dask, _read_geotiff_gpu) from xrspatial.geotiff.tests._helpers.tiff_builders import make_minimal_tiff fsspec = pytest.importorskip("fsspec") @@ -100,9 +100,9 @@ def test_dask_fsspec_source_rejected_under_stable_only(memory_tiff_url): def test_dask_direct_fsspec_source_rejected_under_stable_only(memory_tiff_url): - """The direct ``read_geotiff_dask`` entry point gates remote sources too.""" + """The direct ``_read_geotiff_dask`` entry point gates remote sources too.""" with pytest.raises(RemoteStableSourcesOnlyError) as excinfo: - read_geotiff_dask(memory_tiff_url, chunks=2, stable_only=True) + _read_geotiff_dask(memory_tiff_url, chunks=2, stable_only=True) _assert_remote_stable_error(excinfo) @@ -151,9 +151,9 @@ def test_local_dask_source_succeeds_under_stable_only(local_tiff_path): # --------------------------------------------------------------------------- # Direct GPU entry point (issue #2867) # -# ``read_geotiff_gpu`` is a public direct reader that routes HTTP / fsspec +# ``_read_geotiff_gpu`` is a public direct reader that routes HTTP / fsspec # sources through the CPU fallback, so it must apply the same remote gate as -# ``open_geotiff`` and ``read_geotiff_dask``. The gate runs before the cupy +# ``open_geotiff`` and ``_read_geotiff_dask``. The gate runs before the cupy # import and the CUDA preflight, so the rejection tests run on a CPU-only # machine -- no GPU required. Only the unlock test, which performs a real # read, needs cupy + CUDA. @@ -161,23 +161,23 @@ def test_local_dask_source_succeeds_under_stable_only(local_tiff_path): def test_gpu_direct_fsspec_source_rejected_under_stable_only(memory_tiff_url): - """The direct ``read_geotiff_gpu`` entry point gates remote sources too.""" + """The direct ``_read_geotiff_gpu`` entry point gates remote sources too.""" with pytest.raises(RemoteStableSourcesOnlyError) as excinfo: - read_geotiff_gpu(memory_tiff_url, stable_only=True) + _read_geotiff_gpu(memory_tiff_url, stable_only=True) _assert_remote_stable_error(excinfo) def test_gpu_direct_http_source_rejected_under_stable_only(): """An ``http://`` source is gated on the GPU path before any fetch.""" with pytest.raises(RemoteStableSourcesOnlyError) as excinfo: - read_geotiff_gpu("http://example.invalid/sample.tif", stable_only=True) + _read_geotiff_gpu("http://example.invalid/sample.tif", stable_only=True) _assert_remote_stable_error(excinfo) @_gpu_only def test_gpu_direct_fsspec_source_allowed_with_experimental_optin(memory_tiff_url): """``allow_experimental_codecs=True`` unlocks the advanced tier on GPU.""" - result = read_geotiff_gpu( + result = _read_geotiff_gpu( memory_tiff_url, stable_only=True, allow_experimental_codecs=True, ) assert result.shape == (4, 4) diff --git a/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py b/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py index 2606abb3a..da4bd46c2 100644 --- a/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py +++ b/xrspatial/geotiff/tests/unit/test_codec_roundtrip.py @@ -20,7 +20,7 @@ ``compression_level`` boundaries (eager + dask), availability flag. * Generic ``compression_level``: zstd / deflate round-trip and size monotonicity, LZW silent-ignore, out-of-range rejection. -* ``write_geotiff_gpu`` docstring drift. +* ``_write_geotiff_gpu`` docstring drift. LERC and JPEG 2000 sections use ``pytest.importorskip`` (or skip markers) because the optional codec libraries are not part of the base @@ -1279,7 +1279,7 @@ def test_out_of_range_raises(self, tmp_path, codec, level): # =========================================================================== -# write_geotiff_gpu compression docstring drift +# _write_geotiff_gpu compression docstring drift # =========================================================================== @@ -1312,10 +1312,10 @@ def _gpu_available() -> bool: def test_write_geotiff_gpu_docstring_lists_full_codec_set(): """The ``compression`` docstring block lists every codec ``to_geotiff`` accepts.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu - doc = write_geotiff_gpu.__doc__ - assert doc is not None, "write_geotiff_gpu lost its docstring" + doc = _write_geotiff_gpu.__doc__ + assert doc is not None, "_write_geotiff_gpu lost its docstring" block_start = doc.index("compression : str") block_end = doc.index("compression_level", block_start) block = doc[block_start:block_end] @@ -1334,7 +1334,7 @@ def test_write_geotiff_gpu_accepts_cpu_fallback_codecs(tmp_path, codec): """Codecs without a GPU encoder still write successfully via CPU.""" import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu if codec in ("jpeg2000", "j2k"): arr_cpu = np.random.RandomState(0).randint( @@ -1349,9 +1349,9 @@ def test_write_geotiff_gpu_accepts_cpu_fallback_codecs(tmp_path, codec): "transform": (1.0, 0.0, 0.0, 0.0, -1.0, 64.0)}, ) path = str(tmp_path / f"out_{codec}.tif") - write_geotiff_gpu(da, path, compression=codec, - allow_experimental_codecs=True) + _write_geotiff_gpu(da, path, compression=codec, + allow_experimental_codecs=True) assert os.path.exists(path), ( - f"write_geotiff_gpu(compression={codec!r}) failed to write a file" + f"_write_geotiff_gpu(compression={codec!r}) failed to write a file" ) assert os.path.getsize(path) > 0 diff --git a/xrspatial/geotiff/tests/unit/test_input_validation.py b/xrspatial/geotiff/tests/unit/test_input_validation.py index c710b0ce5..6e0658bd1 100644 --- a/xrspatial/geotiff/tests/unit/test_input_validation.py +++ b/xrspatial/geotiff/tests/unit/test_input_validation.py @@ -9,7 +9,7 @@ ``int`` / ``np.integer``; ``bool`` / ``np.bool_`` raise ``ValueError`` and ``float`` / ``str`` raise ``TypeError``, across every read entry point. -2. Size-parameter validation -- ``tile_size`` and ``read_geotiff_dask`` +2. Size-parameter validation -- ``tile_size`` and ``_read_geotiff_dask`` ``chunks`` must be positive, and ``tile_size`` must be a multiple of 16 when ``tiled=True``. 3. Source-dimension validation -- zero / negative ``ImageWidth`` / @@ -33,8 +33,8 @@ import pytest import xarray as xr -from xrspatial.geotiff import (_header, open_geotiff, read_geotiff_dask, to_geotiff, - write_geotiff_gpu) +from xrspatial.geotiff import (_header, open_geotiff, _read_geotiff_dask, to_geotiff, + _write_geotiff_gpu) from xrspatial.geotiff._coords import coords_to_transform from xrspatial.geotiff._dtypes import LONG, SHORT from xrspatial.geotiff._header import (MAX_PIXEL_ARRAY_COUNT, TAG_BITS_PER_SAMPLE, TAG_COLORMAP, @@ -155,52 +155,52 @@ def test_open_geotiff_band_false_rejected(self, multiband_tiff_path): open_geotiff(path, band=False) def test_read_geotiff_dask_band_true_rejected(self, multiband_tiff_path): - """``read_geotiff_dask(..., band=True)`` rejected before scheduling.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask(..., band=True)`` rejected before scheduling.""" + from xrspatial.geotiff import _read_geotiff_dask path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_dask(path, chunks=4, band=True) + _read_geotiff_dask(path, chunks=4, band=True) def test_read_geotiff_dask_band_false_rejected(self, multiband_tiff_path): - """``read_geotiff_dask(..., band=False)`` raises the same way.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask(..., band=False)`` raises the same way.""" + from xrspatial.geotiff import _read_geotiff_dask path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_dask(path, chunks=4, band=False) + _read_geotiff_dask(path, chunks=4, band=False) @requires_gpu def test_read_geotiff_gpu_band_true_rejected(self, multiband_tiff_path): - """``read_geotiff_gpu(..., band=True)`` is rejected (cupy required).""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(..., band=True)`` is rejected (cupy required).""" + from xrspatial.geotiff import _read_geotiff_gpu path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_gpu(path, band=True) + _read_geotiff_gpu(path, band=True) @requires_gpu def test_read_geotiff_gpu_band_false_rejected(self, multiband_tiff_path): - """``read_geotiff_gpu(..., band=False)`` raises the same way.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(..., band=False)`` raises the same way.""" + from xrspatial.geotiff import _read_geotiff_gpu path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_gpu(path, band=False) + _read_geotiff_gpu(path, band=False) def test_read_vrt_band_true_still_rejected(self, multiband_vrt_path): """VRT path's existing bool rejection remains in place.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt with pytest.raises(ValueError, match="band must be a non-negative int"): - read_vrt(multiband_vrt_path, band=True) + _read_vrt(multiband_vrt_path, band=True) def test_read_vrt_band_false_still_rejected(self, multiband_vrt_path): """VRT path rejects ``band=False`` as well.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt with pytest.raises(ValueError, match="band must be a non-negative int"): - read_vrt(multiband_vrt_path, band=False) + _read_vrt(multiband_vrt_path, band=False) # np.bool_ parity: ``isinstance(np.bool_(True), bool)`` is False so it # bypasses a plain ``isinstance(band, bool)`` guard and is then treated @@ -220,28 +220,28 @@ def test_open_geotiff_band_np_bool_rejected(self, multiband_tiff_path): open_geotiff(path, band=np.bool_(False)) def test_read_geotiff_dask_band_np_bool_rejected(self, multiband_tiff_path): - """``read_geotiff_dask`` rejects ``band=np.bool_(True)``.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask`` rejects ``band=np.bool_(True)``.""" + from xrspatial.geotiff import _read_geotiff_dask path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_dask(path, band=np.bool_(True)) + _read_geotiff_dask(path, band=np.bool_(True)) @requires_gpu def test_read_geotiff_gpu_band_np_bool_rejected(self, multiband_tiff_path): - """``read_geotiff_gpu`` rejects ``band=np.bool_(True)``.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu`` rejects ``band=np.bool_(True)``.""" + from xrspatial.geotiff import _read_geotiff_gpu path, _ = multiband_tiff_path with pytest.raises(ValueError, match="band must be a non-negative int"): - read_geotiff_gpu(path, band=np.bool_(True)) + _read_geotiff_gpu(path, band=np.bool_(True)) def test_read_vrt_band_np_bool_still_rejected(self, multiband_vrt_path): """VRT path already rejects ``np.bool_`` via its integer-type check.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt with pytest.raises(ValueError, match="band must be a non-negative int"): - read_vrt(multiband_vrt_path, band=np.bool_(True)) + _read_vrt(multiband_vrt_path, band=np.bool_(True)) class TestBandTypeRejection: @@ -303,46 +303,46 @@ def test_open_geotiff_band_str_rejected(self, multiband_tiff_path): open_geotiff(path, band="0") def test_read_geotiff_dask_band_float_rejected(self, multiband_tiff_path): - """``read_geotiff_dask(..., band=0.0)`` rejected before scheduling.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask(..., band=0.0)`` rejected before scheduling.""" + from xrspatial.geotiff import _read_geotiff_dask path, _ = multiband_tiff_path with pytest.raises(TypeError, match="band must be a non-negative int"): - read_geotiff_dask(path, chunks=4, band=0.0) + _read_geotiff_dask(path, chunks=4, band=0.0) def test_read_geotiff_dask_band_str_rejected(self, multiband_tiff_path): - """``read_geotiff_dask(..., band='0')`` raises ``TypeError``.""" - from xrspatial.geotiff import read_geotiff_dask + """``_read_geotiff_dask(..., band='0')`` raises ``TypeError``.""" + from xrspatial.geotiff import _read_geotiff_dask path, _ = multiband_tiff_path with pytest.raises(TypeError, match="band must be a non-negative int"): - read_geotiff_dask(path, chunks=4, band="0") + _read_geotiff_dask(path, chunks=4, band="0") def test_read_geotiff_dask_band_int_still_works(self, multiband_tiff_path): """``band=1`` still routes through and reads band 1.""" - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask path, arr = multiband_tiff_path - out = read_geotiff_dask(path, chunks=4, band=1) + out = _read_geotiff_dask(path, chunks=4, band=1) np.testing.assert_array_equal(out.values, arr[:, :, 1]) @requires_gpu def test_read_geotiff_gpu_band_float_rejected(self, multiband_tiff_path): - """``read_geotiff_gpu(..., band=0.0)`` raises ``TypeError``.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(..., band=0.0)`` raises ``TypeError``.""" + from xrspatial.geotiff import _read_geotiff_gpu path, _ = multiband_tiff_path with pytest.raises(TypeError, match="band must be a non-negative int"): - read_geotiff_gpu(path, band=0.0) + _read_geotiff_gpu(path, band=0.0) @requires_gpu def test_read_geotiff_gpu_band_str_rejected(self, multiband_tiff_path): - """``read_geotiff_gpu(..., band='0')`` raises ``TypeError``.""" - from xrspatial.geotiff import read_geotiff_gpu + """``_read_geotiff_gpu(..., band='0')`` raises ``TypeError``.""" + from xrspatial.geotiff import _read_geotiff_gpu path, _ = multiband_tiff_path with pytest.raises(TypeError, match="band must be a non-negative int"): - read_geotiff_gpu(path, band="0") + _read_geotiff_gpu(path, band="0") # =========================================================================== @@ -351,11 +351,11 @@ def test_read_geotiff_gpu_band_str_rejected(self, multiband_tiff_path): # Two writer/reader size parameters used to flow through unchecked: # ``to_geotiff(..., tiled=True, tile_size=0)`` reached the tiled writer # where ``math.ceil(width / tile_size)`` raised a bare ZeroDivisionError, -# and ``read_geotiff_dask(chunks=0)`` propagated zero into dask's chunk +# and ``_read_geotiff_dask(chunks=0)`` propagated zero into dask's chunk # math. Both now validate up front and raise ``ValueError`` naming the # parameter. On top of positivity, ``tile_size`` must be a # multiple of 16 when ``tiled=True`` per the TIFF 6 spec; the error -# suggests the nearest valid value(s). ``write_geotiff_gpu`` is +# suggests the nearest valid value(s). ``_write_geotiff_gpu`` is # always tiled and shares the same check before any cupy import. # =========================================================================== @@ -492,13 +492,13 @@ def test_tile_size_8_message_suggests_16_only(self, tmp_path): assert 'tile_size=0' not in msg def test_write_geotiff_gpu_tile_size_17_rejected(self, tmp_path): - """``write_geotiff_gpu`` shares the multiple-of-16 check with + """``_write_geotiff_gpu`` shares the multiple-of-16 check with ``to_geotiff``. The validation runs before any cupy import, so the bad-tile-size path can be exercised on CPU-only runs.""" da = _make_da() out = os.path.join(str(tmp_path), 'gpu_tile_size_17.tif') with pytest.raises(ValueError) as exc: - write_geotiff_gpu(da, out, tile_size=17) + _write_geotiff_gpu(da, out, tile_size=17) msg = str(exc.value) assert 'tile_size' in msg assert '17' in msg @@ -511,7 +511,7 @@ def test_write_geotiff_gpu_tile_size_zero_rejected(self, tmp_path): da = _make_da() out = os.path.join(str(tmp_path), 'gpu_tile_size_0.tif') with pytest.raises(ValueError, match=r'tile_size.*positive'): - write_geotiff_gpu(da, out, tile_size=0) + _write_geotiff_gpu(da, out, tile_size=0) def test_write_geotiff_gpu_tile_size_float_rejected(self, tmp_path): """``tile_size`` must be an int; floats are rejected by the shared @@ -519,48 +519,48 @@ def test_write_geotiff_gpu_tile_size_float_rejected(self, tmp_path): da = _make_da() out = os.path.join(str(tmp_path), 'gpu_tile_size_float.tif') with pytest.raises(ValueError, match=r'tile_size.*positive int'): - write_geotiff_gpu(da, out, tile_size=256.0) + _write_geotiff_gpu(da, out, tile_size=256.0) class TestReadDaskChunksValidation: - """``read_geotiff_dask(chunks=...)`` must be a positive int or a + """``_read_geotiff_dask(chunks=...)`` must be a positive int or a length-2 tuple of positive ints.""" def test_chunks_zero_raises(self, tmp_path): path = _make_raster(str(tmp_path)) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=0) + _read_geotiff_dask(path, chunks=0) def test_chunks_negative_raises(self, tmp_path): path = _make_raster(str(tmp_path)) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=-1) + _read_geotiff_dask(path, chunks=-1) def test_chunks_tuple_zero_row_raises(self, tmp_path): path = _make_raster(str(tmp_path)) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=(0, 256)) + _read_geotiff_dask(path, chunks=(0, 256)) def test_chunks_tuple_negative_col_raises(self, tmp_path): path = _make_raster(str(tmp_path)) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=(256, -1)) + _read_geotiff_dask(path, chunks=(256, -1)) def test_chunks_tuple_wrong_length_raises(self, tmp_path): path = _make_raster(str(tmp_path)) with pytest.raises(ValueError, match='chunks'): - read_geotiff_dask(path, chunks=(64, 64, 64)) + _read_geotiff_dask(path, chunks=(64, 64, 64)) def test_positive_int_chunks_works(self, tmp_path): path = _make_raster(str(tmp_path)) - arr = read_geotiff_dask(path, chunks=256) + arr = _read_geotiff_dask(path, chunks=256) assert arr.shape == (10, 10) # Materialise to confirm the lazy graph is well-formed. np.asarray(arr) def test_positive_tuple_chunks_works(self, tmp_path): path = _make_raster(str(tmp_path)) - arr = read_geotiff_dask(path, chunks=(4, 8)) + arr = _read_geotiff_dask(path, chunks=(4, 8)) assert arr.shape == (10, 10) np.asarray(arr) @@ -568,13 +568,13 @@ def test_numpy_int_scalar_chunks_works(self, tmp_path): # Numpy integer scalars (e.g. np.int64) should behave like plain # ``int`` for the scalar ``chunks`` form. path = _make_raster(str(tmp_path)) - arr = read_geotiff_dask(path, chunks=np.int64(256)) + arr = _read_geotiff_dask(path, chunks=np.int64(256)) assert arr.shape == (10, 10) np.asarray(arr) def test_numpy_int_tuple_chunks_works(self, tmp_path): path = _make_raster(str(tmp_path)) - arr = read_geotiff_dask(path, chunks=(np.int64(256), 256)) + arr = _read_geotiff_dask(path, chunks=(np.int64(256), 256)) assert arr.shape == (10, 10) np.asarray(arr) @@ -1743,7 +1743,7 @@ def test_gpu_1xN_raises(self, tmp_path): da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "gpu_fail_1xN.tif") with pytest.raises(ValueError, match="(?i)pixel size|transform"): - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) @requires_gpu def test_gpu_Nx1_raises(self, tmp_path): @@ -1754,7 +1754,7 @@ def test_gpu_Nx1_raises(self, tmp_path): da_gpu.attrs = dict(da_cpu.attrs) p = str(tmp_path / "gpu_fail_Nx1.tif") with pytest.raises(ValueError, match="(?i)pixel size|transform"): - write_geotiff_gpu(da_gpu, p) + _write_geotiff_gpu(da_gpu, p) @requires_gpu def test_dask_cupy_1xN_raises(self, tmp_path): diff --git a/xrspatial/geotiff/tests/unit/test_metadata.py b/xrspatial/geotiff/tests/unit/test_metadata.py index f0a1e6990..576b57961 100644 --- a/xrspatial/geotiff/tests/unit/test_metadata.py +++ b/xrspatial/geotiff/tests/unit/test_metadata.py @@ -27,7 +27,7 @@ from xrspatial.geotiff import (ConflictingCRSError, GeoTIFFAmbiguousMetadataError, MixedBandMetadataError, _runtime) from xrspatial.geotiff import _validation as _validation_mod -from xrspatial.geotiff import open_geotiff, read_geotiff_dask, read_vrt, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_geotiff_dask, _read_vrt, to_geotiff from xrspatial.geotiff._attrs import (_ATTRS_CONTRACT_VERSION, GeoTIFFMetadata, _resolve_nodata_attr, attrs_to_metadata, geo_info_to_metadata, metadata_to_attrs) from xrspatial.geotiff._errors import (ConflictingNodataError, InvalidCRSCodeError, @@ -1253,7 +1253,7 @@ def _wrap_2d_1987(arr): def test_read_vrt_rejects_mixed_per_band_nodata_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) with pytest.raises(MixedBandMetadataError) as exc_info: - read_vrt(vrt_path) + _read_vrt(vrt_path) msg = str(exc_info.value) assert "65535" in msg and "65000" in msg assert "band_nodata='first'" in msg @@ -1263,24 +1263,24 @@ def test_read_vrt_rejects_mixed_per_band_nodata_1987(tmp_path): def test_read_vrt_chunked_rejects_mixed_per_band_nodata_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) with pytest.raises(MixedBandMetadataError): - read_vrt(vrt_path, chunks=1) + _read_vrt(vrt_path, chunks=1) def test_read_vrt_band_nodata_first_opts_back_in_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.attrs.get('nodata') == 65535.0 def test_read_vrt_shared_sentinel_accepts_1987(tmp_path): vrt_path = _write_shared_sentinel_vrt_1987(tmp_path) - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.attrs.get('nodata') == 65535.0 def test_read_vrt_only_one_band_declares_sentinel_accepts_1987(tmp_path): vrt_path = _write_one_band_no_sentinel_vrt_1987(tmp_path) - read_vrt(vrt_path) + _read_vrt(vrt_path) def test_open_geotiff_propagates_mixed_band_rejection_1987(tmp_path): @@ -1297,7 +1297,7 @@ def test_open_geotiff_band_nodata_first_passes_through_1987(tmp_path): def test_read_geotiff_dask_band_nodata_first_passes_through_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) - r = read_geotiff_dask(vrt_path, chunks=1, band_nodata='first') + r = _read_geotiff_dask(vrt_path, chunks=1, band_nodata='first') assert r.attrs.get('nodata') == 65535.0 @@ -1316,19 +1316,19 @@ def test_read_geotiff_dask_band_nodata_rejected_on_non_vrt_source_1987(tmp_path) p = tmp_path / 'plain.tif' to_geotiff(arr_da, str(p), compression='none', tiled=False) with pytest.raises(ValueError, match="band_nodata only applies to VRT"): - read_geotiff_dask(str(p), chunks=1, band_nodata='first') + _read_geotiff_dask(str(p), chunks=1, band_nodata='first') def test_mixed_band_metadata_error_subclasses_base_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) with pytest.raises(GeoTIFFAmbiguousMetadataError): - read_vrt(vrt_path) + _read_vrt(vrt_path) def test_read_vrt_band_nodata_rejects_unknown_value_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) with pytest.raises(ValueError, match="band_nodata must be None or 'first'"): - read_vrt(vrt_path, band_nodata='firs') + _read_vrt(vrt_path, band_nodata='firs') def test_open_geotiff_band_nodata_rejects_unknown_value_1987(tmp_path): @@ -1340,7 +1340,7 @@ def test_open_geotiff_band_nodata_rejects_unknown_value_1987(tmp_path): def test_read_geotiff_dask_band_nodata_rejects_unknown_value_1987(tmp_path): vrt_path = _write_mixed_band_vrt_1987(tmp_path) with pytest.raises(ValueError, match="band_nodata must be None or 'first'"): - read_geotiff_dask(vrt_path, chunks=1, band_nodata='banana') + _read_geotiff_dask(vrt_path, chunks=1, band_nodata='banana') # =========================================================================== # Runtime sentinels identity contract (#1880) diff --git a/xrspatial/geotiff/tests/unit/test_photometric.py b/xrspatial/geotiff/tests/unit/test_photometric.py index 2bfef535e..a28153e59 100644 --- a/xrspatial/geotiff/tests/unit/test_photometric.py +++ b/xrspatial/geotiff/tests/unit/test_photometric.py @@ -23,7 +23,7 @@ Section 2 -- ``allow_internal_only_jpeg`` API tier acceptance The CPU dispatcher ``to_geotiff`` exposes the same opt-in flag the - GPU writer ``write_geotiff_gpu`` does, so callers on the auto- + GPU writer ``_write_geotiff_gpu`` does, so callers on the auto- dispatch path can reach the experimental internal-reader-only JPEG encode. The tests pin signature parity, the default-rejection message, the opt-in warning + round-trip, and the GPU dispatch @@ -47,7 +47,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, to_geotiff, write_geotiff_gpu +from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, to_geotiff, _write_geotiff_gpu from xrspatial.geotiff._header import TAG_PHOTOMETRIC, TAG_SAMPLE_FORMAT, parse_header tifffile = pytest.importorskip("tifffile") @@ -362,7 +362,7 @@ def test_gpu_sparse_tile_miniswhite_nodata_zero(tmp_path): Build a tiled MinIsWhite uint8 raster with ``GDAL_NODATA=0``, then patch the first tile's TileOffsets/TileByteCounts to 0 so the read routes through the sparse-tile CPU-fallback branch of - ``read_geotiff_gpu``. Pre-fix this branch discarded + ``_read_geotiff_gpu``. Pre-fix this branch discarded ``_mask_nodata`` and masked against the original sentinel. """ stored = np.zeros((32, 32), dtype=np.uint8) @@ -682,11 +682,11 @@ def _make_rgb_uint8_da() -> xr.DataArray: def test_to_geotiff_signature_has_allow_internal_only_jpeg(): """``to_geotiff`` must expose ``allow_internal_only_jpeg`` so callers on the auto-dispatch path can reach the same opt-in - ``write_geotiff_gpu`` exposes.""" + ``_write_geotiff_gpu`` exposes.""" params = inspect.signature(to_geotiff).parameters assert "allow_internal_only_jpeg" in params, ( "to_geotiff must accept allow_internal_only_jpeg for parity " - "with write_geotiff_gpu." + "with _write_geotiff_gpu." ) assert params["allow_internal_only_jpeg"].default is False @@ -697,7 +697,7 @@ def test_to_geotiff_and_gpu_writer_share_kwarg_default(): eager_default = inspect.signature( to_geotiff).parameters["allow_internal_only_jpeg"].default gpu_default = inspect.signature( - write_geotiff_gpu).parameters["allow_internal_only_jpeg"].default + _write_geotiff_gpu).parameters["allow_internal_only_jpeg"].default assert eager_default == gpu_default == False # noqa: E712 @@ -767,7 +767,7 @@ def test_to_geotiff_non_jpeg_unaffected_by_flag(tmp_path): def test_to_geotiff_gpu_dispatch_forwards_allow_internal_only_jpeg( tmp_path, monkeypatch): """``to_geotiff(gpu=True, allow_internal_only_jpeg=True)`` forwards - the kwarg into ``write_geotiff_gpu``. Without this the GPU writer + the kwarg into ``_write_geotiff_gpu``. Without this the GPU writer would refuse the encode after the CPU dispatcher accepted it.""" captured: dict = {} @@ -779,7 +779,7 @@ def _fake_write_geotiff_gpu(data, path, **kwargs): f.write(b'') monkeypatch.setattr( - 'xrspatial.geotiff._writers.eager.write_geotiff_gpu', + 'xrspatial.geotiff._writers.eager._write_geotiff_gpu', _fake_write_geotiff_gpu, ) @@ -793,11 +793,11 @@ def _fake_write_geotiff_gpu(data, path, **kwargs): ) assert 'kwargs' in captured, ( - "to_geotiff must dispatch to write_geotiff_gpu when gpu=True" + "to_geotiff must dispatch to _write_geotiff_gpu when gpu=True" ) assert captured['kwargs'].get('allow_internal_only_jpeg') is True, ( "to_geotiff must forward allow_internal_only_jpeg unchanged " - "into write_geotiff_gpu." + "into _write_geotiff_gpu." ) assert captured['kwargs'].get('compression') == 'jpeg' @@ -813,7 +813,7 @@ def _fake_write_geotiff_gpu(data, path, **kwargs): if kwargs.get('compression') == 'jpeg' and kwargs.get( 'allow_internal_only_jpeg'): _warnings.warn( - "write_geotiff_gpu jpeg opt-in (stub).", + "_write_geotiff_gpu jpeg opt-in (stub).", GeoTIFFFallbackWarning, stacklevel=2, ) @@ -821,7 +821,7 @@ def _fake_write_geotiff_gpu(data, path, **kwargs): f.write(b'') monkeypatch.setattr( - 'xrspatial.geotiff._writers.eager.write_geotiff_gpu', + 'xrspatial.geotiff._writers.eager._write_geotiff_gpu', _fake_write_geotiff_gpu, ) diff --git a/xrspatial/geotiff/tests/unit/test_predictor.py b/xrspatial/geotiff/tests/unit/test_predictor.py index 3cadd0ae4..bc1e9a037 100644 --- a/xrspatial/geotiff/tests/unit/test_predictor.py +++ b/xrspatial/geotiff/tests/unit/test_predictor.py @@ -456,7 +456,7 @@ def test_gpu_predictor2_int8_matches_cpu(tmp_path, tiled): """ import cupy - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = _signed_int8_grid(32, 48) if tiled else _signed_int8_grid() path = tmp_path / f"pred2_int8_{'tiled' if tiled else 'stripped'}_gpu.tif" @@ -468,7 +468,7 @@ def test_gpu_predictor2_int8_matches_cpu(tmp_path, tiled): cpu, _ = read_to_array(str(path)) np.testing.assert_array_equal(cpu, arr) - gpu_da = read_geotiff_gpu(str(path)) + gpu_da = _read_geotiff_gpu(str(path)) assert isinstance(gpu_da.data, cupy.ndarray) assert gpu_da.data.dtype == np.int8 np.testing.assert_array_equal(gpu_da.data.get(), cpu) @@ -1198,9 +1198,9 @@ def test_open_geotiff_dask_raises(self, tmp_path): path = tmp_path / "pred3_uint32_dask.tif" path.write_bytes(_build_predictor3_uint32_tiff(arr)) - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask with pytest.raises(ValueError, match="Predictor=3"): - read_geotiff_dask(str(path), chunks=64) + _read_geotiff_dask(str(path), chunks=64) # =========================================================================== diff --git a/xrspatial/geotiff/tests/unit/test_safe_xml.py b/xrspatial/geotiff/tests/unit/test_safe_xml.py index 8dac5aa68..12290072d 100644 --- a/xrspatial/geotiff/tests/unit/test_safe_xml.py +++ b/xrspatial/geotiff/tests/unit/test_safe_xml.py @@ -136,7 +136,7 @@ def test_to_geotiff_special_chars_round_trip(self, tmp_path): # TIFF with a SubIFDs entry leaked those tags into # ``attrs['extra_tags']`` because they were not in ``_MANAGED_TAGS``. # Writing the DataArray back via ``to_geotiff`` or -# ``write_geotiff_gpu`` then re-emitted them on the output IFD, +# ``_write_geotiff_gpu`` then re-emitted them on the output IFD, # producing: # # * A primary IFD wrongly marked as a reduced-resolution overview diff --git a/xrspatial/geotiff/tests/unit/test_signatures.py b/xrspatial/geotiff/tests/unit/test_signatures.py index 7114039a9..542e29ba0 100644 --- a/xrspatial/geotiff/tests/unit/test_signatures.py +++ b/xrspatial/geotiff/tests/unit/test_signatures.py @@ -36,10 +36,10 @@ Section 6 -- Reader / writer kwarg behaviour Override-effect and dtype-cast coverage for kwargs that the - signature pins above only assert as *accepted*: ``read_geotiff_gpu`` - / ``read_geotiff_dask`` ``name`` and ``max_pixels``, ``write_vrt`` + signature pins above only assert as *accepted*: ``_read_geotiff_gpu`` + / ``_read_geotiff_dask`` ``name`` and ``max_pixels``, ``build_vrt`` ``relative`` / ``crs`` / ``nodata``, GPU reader ``dtype``, GPU writer - ``bigtiff`` / ``predictor``, and ``read_vrt`` ``window``. + ``bigtiff`` / ``predictor``, and ``_read_vrt`` ``window``. The sections share a *concern* (the public API contract) rather than runtime logic. GPU rows skip when cupy + CUDA are absent via the shared @@ -63,8 +63,8 @@ import xrspatial.geotiff as g import xrspatial.geotiff._compression as comp_mod from xrspatial.geotiff import (GeoTIFFFallbackWarning, _geotiff_strict_mode, _wkt_to_epsg, - open_geotiff, read_geotiff_dask, read_geotiff_gpu, read_vrt, - to_geotiff, write_geotiff_gpu, write_vrt) + open_geotiff, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, + to_geotiff, _write_geotiff_gpu, build_vrt) from xrspatial.geotiff._attrs import (_COMPRESSION_TAG_TO_NAME, _validate_read_codec_optin, _validate_write_rich_tag_optin) from xrspatial.geotiff._compression import (_HAVE_LIBDEFLATE, COMPRESSION_DEFLATE, COMPRESSION_LZ4, @@ -112,17 +112,17 @@ def test_open_geotiff_window_annotated(): def test_read_vrt_window_annotated(): - assert _annotation(read_vrt, 'window') == 'tuple | None' + 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' + 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' + assert _annotation(_read_geotiff_gpu, 'window') == 'tuple | None' # --- path: str or binary file-like (writer entry points) --- @@ -136,22 +136,22 @@ def test_to_geotiff_path_annotated(): def test_write_geotiff_gpu_path_annotated(): - """``write_geotiff_gpu(data, path, ...)`` ``path`` mirrors ``to_geotiff``.""" - ann = _annotation(write_geotiff_gpu, 'path') + """``_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_path_annotated(): - """``write_vrt(path, ...)`` is str-only (VRT writes are path-only by + """``build_vrt(path, ...)`` is str-only (VRT writes are path-only by design; no file-like buffer support). The canonical name - is ``path`` (parity with ``to_geotiff`` / ``write_geotiff_gpu``). + is ``path`` (parity with ``to_geotiff`` / ``_write_geotiff_gpu``). The annotation is plain ``str``: the default value is a private sentinel (not ``None``) so the deprecation shim can distinguish - ``write_vrt(path=None, ...)`` (rejected with TypeError) from a + ``build_vrt(path=None, ...)`` (rejected with TypeError) from a caller who omitted ``path`` entirely (routed through the ``vrt_path`` alias).""" - assert _annotation(write_vrt, 'path') == 'str' + assert _annotation(build_vrt, 'path') == 'str' def test_write_vrt_vrt_path_annotated(): @@ -159,7 +159,7 @@ def test_write_vrt_vrt_path_annotated(): annotation as ``path`` (str-only at the type level; ``None`` only appears because the sentinel default lets the shim detect omission). Pinned so a future re-rename does not silently widen the alias.""" - assert _annotation(write_vrt, 'vrt_path') == 'str | None' + assert _annotation(build_vrt, 'vrt_path') == 'str | None' # --- source: str or BinaryIO (open_geotiff is the public dispatch) --- @@ -178,22 +178,22 @@ def test_open_geotiff_source_annotated(): def test_read_geotiff_dask_source_str_only(): - """``read_geotiff_dask(source: str)`` stays str-only: the dask path + """``_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' + 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 + """``_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' + 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 + """``_read_vrt(source: str)`` stays str-only: the VRT XML references its own source files on disk.""" - assert _annotation(read_vrt, 'source') == 'str' + assert _annotation(_read_vrt, 'source') == 'str' # --- dtype: str | np.dtype | None on every reader entry point --- @@ -207,15 +207,15 @@ def test_open_geotiff_dtype_annotated(): def test_read_geotiff_dask_dtype_annotated(): - assert _annotation(read_geotiff_dask, 'dtype') == 'str | np.dtype | None' + assert _annotation(_read_geotiff_dask, 'dtype') == 'str | np.dtype | None' def test_read_geotiff_gpu_dtype_annotated(): - assert _annotation(read_geotiff_gpu, 'dtype') == 'str | np.dtype | None' + assert _annotation(_read_geotiff_gpu, 'dtype') == 'str | np.dtype | None' def test_read_vrt_dtype_annotated(): - assert _annotation(read_vrt, 'dtype') == 'str | np.dtype | None' + assert _annotation(_read_vrt, 'dtype') == 'str | np.dtype | None' # --- on_gpu_failure: 'auto' | 'strict' (GPU failure policy) --- @@ -226,13 +226,13 @@ def test_open_geotiff_on_gpu_failure_annotated(): def test_read_geotiff_gpu_on_gpu_failure_annotated(): - assert _annotation(read_geotiff_gpu, 'on_gpu_failure') == 'str' + 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 + """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' + assert _annotation(_read_geotiff_gpu, 'gpu') == 'str' # --- nodata: float | int | None on every writer entry point --- @@ -243,12 +243,12 @@ def test_to_geotiff_nodata_annotated(): def test_write_geotiff_gpu_nodata_annotated(): - assert _annotation(write_geotiff_gpu, 'nodata') == 'float | int | None' + assert _annotation(_write_geotiff_gpu, 'nodata') == 'float | int | None' def test_write_vrt_nodata_annotated(): """Pre-existing annotation -- keep it pinned.""" - assert _annotation(write_vrt, 'nodata') == 'float | int | None' + assert _annotation(build_vrt, 'nodata') == 'float | int | None' # --- streaming_buffer_bytes: int on both writer entry points --- @@ -271,10 +271,10 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_annotated(): hint. The kwarg is a runtime no-op on the GPU writer (deleted on entry); the annotation parity is the only consistency dimension.""" assert _annotation( - write_geotiff_gpu, 'streaming_buffer_bytes' + _write_geotiff_gpu, 'streaming_buffer_bytes' ) == 'int' assert ( - inspect.signature(write_geotiff_gpu) + inspect.signature(_write_geotiff_gpu) .parameters['streaming_buffer_bytes'] .default == 256 * 1024 * 1024 @@ -376,8 +376,8 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_runtime_noop(tmp_path): ) p1 = str(tmp_path / 'default.tif') p2 = str(tmp_path / 'override.tif') - write_geotiff_gpu(da_gpu, p1) - write_geotiff_gpu(da_gpu, p2, streaming_buffer_bytes=8 * 1024 * 1024) + _write_geotiff_gpu(da_gpu, p1) + _write_geotiff_gpu(da_gpu, p2, streaming_buffer_bytes=8 * 1024 * 1024) # Both files have identical sizes -- the buffer kwarg is a no-op. assert os.path.getsize(p1) == os.path.getsize(p2) @@ -387,7 +387,7 @@ def test_write_geotiff_gpu_streaming_buffer_bytes_runtime_noop(tmp_path): # =========================================================================== # # ``open_geotiff`` is the canonical surface. The three backend readers -# (``read_geotiff_gpu``, ``read_geotiff_dask``, ``read_vrt``) must list the +# (``_read_geotiff_gpu``, ``_read_geotiff_dask``, ``_read_vrt``) must list the # shared kwargs in the same relative order so ``inspect.signature``, IDE # autocomplete, and Sphinx-rendered docs do not drift. Each per-backend # signature carries its own subset of the canonical parameter list; @@ -447,7 +447,7 @@ def _assert_canonical(fn, allowed_tail=()): Parameters that appear in ``_CANONICAL_ORDER`` must show up in the same relative order. Extras (e.g. the deprecated ``gpu`` alias on - ``read_geotiff_gpu``) are accepted at the tail when listed in + ``_read_geotiff_gpu``) are accepted at the tail when listed in ``allowed_tail`` and otherwise rejected so new kwargs cannot be quietly added in arbitrary positions. """ @@ -478,55 +478,55 @@ def test_open_geotiff_defines_canonical_order(): def test_read_geotiff_gpu_matches_canonical_order(): - """``read_geotiff_gpu`` must list shared params in the canonical order.""" + """``_read_geotiff_gpu`` must list shared params in the canonical order.""" # ``gpu`` here is the deprecated alias for ``on_gpu_failure`` (see - # ``read_geotiff_gpu``'s docstring). It is not the boolean backend - # selector that lives on ``open_geotiff`` / ``read_vrt``, so it sits + # ``_read_geotiff_gpu``'s docstring). It is not the boolean backend + # selector that lives on ``open_geotiff`` / ``_read_vrt``, so it sits # at the tail rather than in its canonical-order slot. - params = _kwonly_params(read_geotiff_gpu) + params = _kwonly_params(_read_geotiff_gpu) # ``gpu`` is the deprecated alias, intentionally last. assert params[-1] == "gpu", ( - f"read_geotiff_gpu must keep the deprecated 'gpu' alias as the last " + f"_read_geotiff_gpu must keep the deprecated 'gpu' alias as the last " f"kwarg; got {params!r}" ) # Drop the alias and run the canonical-subset check on the rest. head = params[:-1] canonical_head = [p for p in _CANONICAL_ORDER if p in head] assert head == canonical_head, ( - f"read_geotiff_gpu kwarg order {head!r} does not match the canonical " + f"_read_geotiff_gpu kwarg order {head!r} does not match the canonical " f"subset {canonical_head!r}" ) def test_read_geotiff_dask_matches_canonical_order(): - """``read_geotiff_dask`` must list shared params in the canonical order.""" - _assert_canonical(read_geotiff_dask) + """``_read_geotiff_dask`` must list shared params in the canonical order.""" + _assert_canonical(_read_geotiff_dask) def test_read_vrt_matches_canonical_order(): - """``read_vrt`` must list shared params in the canonical order. + """``_read_vrt`` must list shared params in the canonical order. ``band_nodata`` is the opt-out for the mixed-band metadata check; it is VRT-specific (no analogue on the other readers) and so lives in the per-function tail rather than in the shared canonical order. """ - _assert_canonical(read_vrt, allowed_tail=('band_nodata',)) + _assert_canonical(_read_vrt, allowed_tail=('band_nodata',)) def test_no_pairwise_order_inversions(): """For any pair of params shared by two readers, the order is consistent. - ``read_geotiff_gpu``'s ``gpu`` kwarg is a deprecated alias for + ``_read_geotiff_gpu``'s ``gpu`` kwarg is a deprecated alias for ``on_gpu_failure`` rather than the boolean backend selector that - ``open_geotiff`` / ``read_vrt`` expose, so it is excluded from the + ``open_geotiff`` / ``_read_vrt`` expose, so it is excluded from the cross-reader pair check. """ - readers = (open_geotiff, read_geotiff_gpu, read_geotiff_dask, read_vrt) + readers = (open_geotiff, _read_geotiff_gpu, _read_geotiff_dask, _read_vrt) orders = {} for fn in readers: params = _kwonly_params(fn) - if fn is read_geotiff_gpu: + if fn is _read_geotiff_gpu: # Drop the deprecated alias before cross-comparing with the other # readers' boolean ``gpu`` kwarg (different meaning, same name). params = [p for p in params if p != "gpu"] @@ -608,9 +608,9 @@ def _write_test_tif(tmp_path, compression: str, @pytest.mark.parametrize( - "fn", [open_geotiff, read_geotiff_dask, read_geotiff_gpu]) + "fn", [open_geotiff, _read_geotiff_dask, _read_geotiff_gpu]) def test_read_signature_has_codec_optin(fn): - """``open_geotiff`` / ``read_geotiff_dask`` / ``read_geotiff_gpu`` + """``open_geotiff`` / ``_read_geotiff_dask`` / ``_read_geotiff_gpu`` expose ``allow_experimental_codecs=False`` and ``allow_internal_only_jpeg=False``. The default is ``False`` so accidental removal of the gate would surface here. @@ -842,7 +842,7 @@ def test_read_geotiff_dask_rejects_experimental_codec(tmp_path): path = _write_test_tif( tmp_path, 'lz4', allow_experimental_codecs=True) with pytest.raises(ValueError, match='allow_experimental_codecs'): - read_geotiff_dask(path, chunks=16) + _read_geotiff_dask(path, chunks=16) def test_read_geotiff_dask_accepts_experimental_codec_with_flag(tmp_path): @@ -850,7 +850,7 @@ def test_read_geotiff_dask_accepts_experimental_codec_with_flag(tmp_path): path = _write_test_tif( tmp_path, 'lz4', allow_experimental_codecs=True) try: - da = read_geotiff_dask( + da = _read_geotiff_dask( path, chunks=16, allow_experimental_codecs=True) except (ImportError, ModuleNotFoundError) as e: pytest.skip(f"optional decoder missing: {e}") @@ -915,7 +915,7 @@ def test_write_geotiff_gpu_rejects_rich_tags_without_flag(tmp_path): ) path = os.path.join(str(tmp_path), 'rich_gpu.tif') with pytest.raises(ValueError, match='gdal_metadata_xml'): - write_geotiff_gpu(da, path) + _write_geotiff_gpu(da, path) # --- Already-gated paths: pin the existing opt-in inventory --- @@ -1751,12 +1751,12 @@ def test_write_deflate_round_trip_across_parallelism_modes( # Override-effect and dtype-cast coverage for kwargs that the signature # pins in earlier sections assert only as *accepted*. Three groups: # -# 6a -- ``write_vrt`` ``relative`` / ``crs`` / ``nodata`` override effect, +# 6a -- ``build_vrt`` ``relative`` / ``crs`` / ``nodata`` override effect, # plus the empty-``source_files`` error path. -# 6b -- ``read_geotiff_gpu`` / ``read_geotiff_dask`` ``name`` and -# ``max_pixels``, ``read_geotiff_gpu`` ``dtype`` cast, GPU writer +# 6b -- ``_read_geotiff_gpu`` / ``_read_geotiff_dask`` ``name`` and +# ``max_pixels``, ``_read_geotiff_gpu`` ``dtype`` cast, GPU writer # ``bigtiff``. -# 6c -- GPU writer ``predictor`` encode kernels and ``read_vrt(window=)`` +# 6c -- GPU writer ``predictor`` encode kernels and ``_read_vrt(window=)`` # windowed-read semantics. @@ -1819,7 +1819,7 @@ def small_tiff_path(tmp_path): return str(p), arr -# --- 6a: write_vrt override effect (relative / crs / nodata) + error path --- +# --- 6a: build_vrt override effect (relative / crs / nodata) + error path --- class TestWriteVrtRelativeBehaviour: @@ -1833,7 +1833,7 @@ def _read_xml(self, path): def test_relative_true_writes_relative_path(self, source_tif, tmp_path): vrt_path = str(tmp_path / 'rel_true.vrt') - write_vrt(vrt_path, [source_tif], relative=True) + build_vrt(vrt_path, [source_tif], relative=True) xml = self._read_xml(vrt_path) # The on-disk text must carry the relativeToVRT="1" attribute, @@ -1850,7 +1850,7 @@ def test_relative_true_writes_relative_path(self, source_tif, tmp_path): def test_relative_false_writes_absolute_path(self, source_tif, tmp_path): vrt_path = str(tmp_path / 'rel_false.vrt') - write_vrt(vrt_path, [source_tif], relative=False) + build_vrt(vrt_path, [source_tif], relative=False) xml = self._read_xml(vrt_path) # ``relative=False`` must flip the attribute and emit an absolute @@ -1867,7 +1867,7 @@ def test_relative_true_parses_back_to_same_source(self, source_tif, tmp_path): """relative=True still round-trips: parse_vrt resolves the relative path back to the absolute one.""" vrt_path = str(tmp_path / 'rel_true_rt.vrt') - write_vrt(vrt_path, [source_tif], relative=True) + build_vrt(vrt_path, [source_tif], relative=True) parsed = parse_vrt(self._read_xml(vrt_path), vrt_dir=str(tmp_path)) assert len(parsed.bands) == 1 assert len(parsed.bands[0].sources) == 1 @@ -1880,7 +1880,7 @@ def test_relative_true_parses_back_to_same_source(self, source_tif, tmp_path): def test_relative_false_parses_back_to_same_source(self, source_tif, tmp_path): vrt_path = str(tmp_path / 'rel_false_rt.vrt') - write_vrt(vrt_path, [source_tif], relative=False) + build_vrt(vrt_path, [source_tif], relative=False) parsed = parse_vrt(self._read_xml(vrt_path), vrt_dir=str(tmp_path)) assert len(parsed.bands) == 1 assert ( @@ -1895,7 +1895,7 @@ class TestWriteVrtCrsWktBehaviour: override wins. The kwarg was formerly named ``crs_wkt``. The new canonical name - is ``crs`` (parity with ``to_geotiff`` / ``write_geotiff_gpu``); + is ``crs`` (parity with ``to_geotiff`` / ``_write_geotiff_gpu``); the old name is still accepted with ``DeprecationWarning``. These tests exercise the new path; the deprecated path is covered by the VRT write tests. @@ -1914,7 +1914,7 @@ def test_crs_wkt_override_wins(self, source_tif, tmp_path): 'PROJECTION["Transverse_Mercator"],UNIT["metre",1]]' ) vrt_path = str(tmp_path / 'crs_wkt_override.vrt') - write_vrt(vrt_path, [source_tif], crs=override) + build_vrt(vrt_path, [source_tif], crs=override) parsed = self._read_parsed(vrt_path, tmp_path) assert parsed.crs_wkt == override @@ -1924,7 +1924,7 @@ def test_crs_wkt_none_falls_back_to_first_source(self, source_tif, tmp_path): non-empty, and match the source TIF's own crs_wkt (no silent substitution, no None on the fall-back path).""" vrt_path = str(tmp_path / 'crs_wkt_default.vrt') - write_vrt(vrt_path, [source_tif]) + build_vrt(vrt_path, [source_tif]) parsed = self._read_parsed(vrt_path, tmp_path) source_da = open_geotiff(source_tif) @@ -1947,10 +1947,10 @@ def test_crs_wkt_override_distinct_from_default(self, source_tif, tmp_path): ) # Override path vrt_override = str(tmp_path / 'override.vrt') - write_vrt(vrt_override, [source_tif], crs=override) + build_vrt(vrt_override, [source_tif], crs=override) # Default path vrt_default = str(tmp_path / 'default.vrt') - write_vrt(vrt_default, [source_tif]) + build_vrt(vrt_default, [source_tif]) with open(vrt_override, 'r') as fh: text_override = fh.read() @@ -1972,7 +1972,7 @@ def _bands(self, vrt_path, tmp_path): def test_nodata_override_wins(self, source_tif, tmp_path): vrt_path = str(tmp_path / 'nodata_override.vrt') - write_vrt(vrt_path, [source_tif], nodata=-9999.0) + build_vrt(vrt_path, [source_tif], nodata=-9999.0) bands = self._bands(vrt_path, tmp_path) assert len(bands) == 1 assert bands[0].nodata == -9999.0 @@ -1983,7 +1983,7 @@ def test_nodata_none_takes_first_source(self, source_tif, tmp_path): silently dropped the default-from-source code path would land ``None`` here.""" vrt_path = str(tmp_path / 'nodata_default.vrt') - write_vrt(vrt_path, [source_tif]) + build_vrt(vrt_path, [source_tif]) bands = self._bands(vrt_path, tmp_path) assert len(bands) == 1 assert bands[0].nodata == -1.0 @@ -1992,14 +1992,14 @@ def test_nodata_override_writes_xml_element(self, source_tif, tmp_path): """Raw XML check: the override sentinel value lands in a element.""" vrt_path = str(tmp_path / 'nodata_xml.vrt') - write_vrt(vrt_path, [source_tif], nodata=-12345.0) + build_vrt(vrt_path, [source_tif], nodata=-12345.0) with open(vrt_path, 'r') as fh: xml = fh.read() assert '-12345.0' in xml class TestWriteVrtEmptySourceFiles: - """``write_vrt(source_files=[])`` raises with a clear message. + """``build_vrt(source_files=[])`` raises with a clear message. The error path is uncovered. A regression dropping the pre-validation would surface much further down as an IndexError when computing the bounding box of zero sources.""" @@ -2007,12 +2007,12 @@ class TestWriteVrtEmptySourceFiles: def test_empty_list_raises(self, tmp_path): vrt_path = str(tmp_path / 'should_not_exist.vrt') with pytest.raises(ValueError, match="source_files must not be empty"): - write_vrt(vrt_path, []) + build_vrt(vrt_path, []) def test_empty_list_does_not_create_file(self, tmp_path): vrt_path = str(tmp_path / 'should_not_exist_2.vrt') try: - write_vrt(vrt_path, []) + build_vrt(vrt_path, []) except ValueError: pass assert not os.path.exists(vrt_path) @@ -2023,14 +2023,14 @@ def test_empty_list_does_not_create_file(self, tmp_path): def test_read_geotiff_dask_name_kwarg_sets_name(small_tiff_path): path, arr = small_tiff_path - da = read_geotiff_dask(path, chunks=4, name="custom_dask") + da = _read_geotiff_dask(path, chunks=4, name="custom_dask") assert da.name == "custom_dask" np.testing.assert_array_equal(da.values, arr) def test_read_geotiff_dask_default_name_from_path(small_tiff_path): path, _ = small_tiff_path - da = read_geotiff_dask(path, chunks=4) + da = _read_geotiff_dask(path, chunks=4) # Default name is filename stem when no override is supplied. assert da.name == "small" @@ -2038,7 +2038,7 @@ def test_read_geotiff_dask_default_name_from_path(small_tiff_path): @requires_gpu def test_read_geotiff_gpu_name_kwarg_sets_name(small_tiff_path): path, arr = small_tiff_path - da = read_geotiff_gpu(path, name="custom_gpu") + da = _read_geotiff_gpu(path, name="custom_gpu") assert da.name == "custom_gpu" np.testing.assert_array_equal(da.data.get(), arr) @@ -2046,14 +2046,14 @@ def test_read_geotiff_gpu_name_kwarg_sets_name(small_tiff_path): @requires_gpu def test_read_geotiff_gpu_default_name_from_path(small_tiff_path): path, _ = small_tiff_path - da = read_geotiff_gpu(path) + da = _read_geotiff_gpu(path) assert da.name == "small" @requires_gpu def test_read_geotiff_gpu_chunks_name_kwarg_sets_name(small_tiff_path): path, arr = small_tiff_path - da = read_geotiff_gpu(path, chunks=4, name="custom_dask_gpu") + da = _read_geotiff_gpu(path, chunks=4, name="custom_dask_gpu") assert da.name == "custom_dask_gpu" np.testing.assert_array_equal(da.data.compute().get(), arr) @@ -2064,7 +2064,7 @@ def test_read_geotiff_gpu_max_pixels_accepts_within_budget(small_tiff_path): # 8 * 8 = 64 pixels but per-tile dim safety check uses tile_size=16 # (256 pixels per tile); 300 leaves room. The fixture's tile_size # was bumped to 16 to satisfy the TIFF 6 multiple-of-16 rule. - da = read_geotiff_gpu(path, max_pixels=300) + da = _read_geotiff_gpu(path, max_pixels=300) np.testing.assert_array_equal(da.data.get(), arr) @@ -2072,7 +2072,7 @@ def test_read_geotiff_gpu_max_pixels_accepts_within_budget(small_tiff_path): def test_read_geotiff_gpu_max_pixels_rejects_oversized(small_tiff_path): path, _ = small_tiff_path with pytest.raises(ValueError, match="safety limit|exceeds max_pixels"): - read_geotiff_gpu(path, max_pixels=10) + _read_geotiff_gpu(path, max_pixels=10) @requires_gpu @@ -2089,7 +2089,7 @@ def test_read_geotiff_gpu_chunks_max_pixels_rejects_oversized(small_tiff_path): """ path, _ = small_tiff_path with pytest.raises(ValueError, match="safety limit|exceeds max_pixels"): - da = read_geotiff_gpu(path, chunks=4, max_pixels=10) + da = _read_geotiff_gpu(path, chunks=4, max_pixels=10) da.compute() @@ -2125,31 +2125,31 @@ def test_open_geotiff_gpu_max_pixels_rejects(small_tiff_path): @requires_gpu class TestReadGeotiffGpuDtype: - """``read_geotiff_gpu(dtype=...)`` casts on device. The eager CPU + """``_read_geotiff_gpu(dtype=...)`` casts on device. The eager CPU path has TestDtypeEager; the dask path has TestDtypeDask. The GPU path had no equivalent.""" def test_float64_to_float32(self, float64_tif): path, orig = float64_tif - result = read_geotiff_gpu(path, dtype='float32') + result = _read_geotiff_gpu(path, dtype='float32') assert result.dtype == np.float32 np.testing.assert_array_almost_equal( result.data.get(), orig.astype(np.float32), decimal=6) def test_float64_to_float16(self, float64_tif): path, _ = float64_tif - result = read_geotiff_gpu(path, dtype=np.float16) + result = _read_geotiff_gpu(path, dtype=np.float16) assert result.dtype == np.float16 def test_uint16_to_int32(self, uint16_tif): path, orig = uint16_tif - result = read_geotiff_gpu(path, dtype='int32') + result = _read_geotiff_gpu(path, dtype='int32') assert result.dtype == np.int32 np.testing.assert_array_equal(result.data.get(), orig.astype(np.int32)) def test_uint16_to_uint8(self, uint16_tif): path, _ = uint16_tif - result = read_geotiff_gpu(path, dtype='uint8') + result = _read_geotiff_gpu(path, dtype='uint8') assert result.dtype == np.uint8 def test_float_to_int_raises(self, float64_tif): @@ -2157,23 +2157,23 @@ def test_float_to_int_raises(self, float64_tif): # The validator runs before the GPU upload; the error contract is # the same as the CPU path (``float`` ... ``int``). with pytest.raises(ValueError, match='float.*int'): - read_geotiff_gpu(path, dtype='int32') + _read_geotiff_gpu(path, dtype='int32') def test_dtype_none_preserves_native_float64(self, float64_tif): path, _ = float64_tif - result = read_geotiff_gpu(path, dtype=None) + result = _read_geotiff_gpu(path, dtype=None) assert result.dtype == np.float64 def test_dtype_none_preserves_native_uint16(self, uint16_tif): path, _ = uint16_tif - result = read_geotiff_gpu(path, dtype=None) + result = _read_geotiff_gpu(path, dtype=None) assert result.dtype == np.uint16 @requires_gpu class TestOpenGeotiffGpuDispatchDtype: """``open_geotiff(..., gpu=True, dtype=...)`` forwards through the - dispatcher into ``read_geotiff_gpu``. Pin the dispatch path so a + dispatcher into ``_read_geotiff_gpu``. Pin the dispatch path so a regression dropping ``dtype=`` on the GPU branch surfaces here too.""" def test_dispatch_float64_to_float32(self, float64_tif): @@ -2191,14 +2191,14 @@ def test_dispatch_float_to_int_raises(self, float64_tif): @requires_gpu class TestReadGeotiffGpuChunksDtype: - """``read_geotiff_gpu(chunks=..., dtype=...)`` -- dask + GPU + dtype + """``_read_geotiff_gpu(chunks=..., dtype=...)`` -- dask + GPU + dtype combination is a separate dispatch path through the GPU reader and its own ``astype`` step on the cupy array, then a ``chunk`` call. Cover the cast for the dask+GPU branch too.""" def test_chunks_float64_to_float32(self, float64_tif): path, orig = float64_tif - result = read_geotiff_gpu(path, chunks=20, dtype='float32') + result = _read_geotiff_gpu(path, chunks=20, dtype='float32') assert result.dtype == np.float32 # ``.data`` is a dask array of cupy chunks. Compute, then # ``.get()`` the resulting cupy host buffer. @@ -2209,7 +2209,7 @@ def test_chunks_float64_to_float32(self, float64_tif): @requires_gpu class TestWriteGeotiffGpuBigtiff: - """``write_geotiff_gpu(bigtiff=)`` threads ``force_bigtiff=`` to + """``_write_geotiff_gpu(bigtiff=)`` threads ``force_bigtiff=`` to ``_assemble_tiff``. The CPU writer has equivalent header-level bigtiff coverage; the GPU writer did not. @@ -2232,9 +2232,9 @@ def test_force_bigtiff_true_writes_bigtiff(self, tmp_path): 'x': np.arange(8, dtype=np.float64)}, ) path = str(tmp_path / 'gpu_bigtiff_true.tif') - write_geotiff_gpu(da, path, bigtiff=True, tile_size=16) + _write_geotiff_gpu(da, path, bigtiff=True, tile_size=16) assert self._read_header_is_bigtiff(path), ( - "write_geotiff_gpu(bigtiff=True) should emit BigTIFF header " + "_write_geotiff_gpu(bigtiff=True) should emit BigTIFF header " "(magic byte 43)." ) # Data round-trips even with the BigTIFF header. @@ -2250,9 +2250,9 @@ def test_force_bigtiff_false_writes_classic(self, tmp_path): 'x': np.arange(8, dtype=np.float64)}, ) path = str(tmp_path / 'gpu_bigtiff_false.tif') - write_geotiff_gpu(da, path, bigtiff=False, tile_size=16) + _write_geotiff_gpu(da, path, bigtiff=False, tile_size=16) assert not self._read_header_is_bigtiff(path), ( - "write_geotiff_gpu(bigtiff=False) should emit classic TIFF." + "_write_geotiff_gpu(bigtiff=False) should emit classic TIFF." ) def test_bigtiff_none_stays_classic_small_file(self, tmp_path): @@ -2268,16 +2268,16 @@ def test_bigtiff_none_stays_classic_small_file(self, tmp_path): 'x': np.arange(8, dtype=np.float64)}, ) path = str(tmp_path / 'gpu_bigtiff_default.tif') - write_geotiff_gpu(da, path, tile_size=16) + _write_geotiff_gpu(da, path, tile_size=16) assert not self._read_header_is_bigtiff(path), ( - "write_geotiff_gpu default should auto-pick classic TIFF for " + "_write_geotiff_gpu default should auto-pick classic TIFF for " "tiny outputs; a default switch to BigTIFF would break " "older readers." ) def test_to_geotiff_gpu_bigtiff_threads_through(self, tmp_path): """``to_geotiff(..., gpu=True, bigtiff=True)`` dispatches into - ``write_geotiff_gpu(bigtiff=True)``. Cover the dispatcher's + ``_write_geotiff_gpu(bigtiff=True)``. Cover the dispatcher's thread-through so a regression dropping ``bigtiff=`` on the GPU dispatch branch surfaces here too.""" import cupy @@ -2297,7 +2297,7 @@ def test_to_geotiff_gpu_bigtiff_threads_through(self, tmp_path): np.testing.assert_array_equal(rd.values, arr.get()) -# --- 6c: GPU writer predictor encode kernels + read_vrt(window=) --- +# --- 6c: GPU writer predictor encode kernels + _read_vrt(window=) --- def _read_predictor_tag(path: str) -> int | None: @@ -2358,8 +2358,8 @@ def test_predictor_true_uint8_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred2_u8_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=True, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=True, + tile_size=16) # Round-trip through the public reader out = open_geotiff(path) @@ -2375,8 +2375,8 @@ def test_predictor_2_uint8_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred2_int_u8_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=2, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=2, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2397,8 +2397,8 @@ def test_predictor_2_uint8_3band_rgb(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred2_u8_3band_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=2, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=2, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2416,8 +2416,8 @@ def test_predictor_false_no_predictor_tag(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_no_pred_u8_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=False, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=False, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2440,8 +2440,8 @@ def test_predictor_2_uint16_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred2_u16_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=2, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=2, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2467,8 +2467,8 @@ def test_predictor_2_int32_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred2_i32_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=2, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=2, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2495,8 +2495,8 @@ def test_predictor_3_float32_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred3_f32_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=3, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=3, + tile_size=16) out = open_geotiff(path) # FP predictor is lossless: equality, not allclose @@ -2510,8 +2510,8 @@ def test_predictor_3_float64_round_trip(self, tmp_path): da = _da_with_float_coords(cupy.asarray(arr)) path = str(tmp_path / 'gpu_pred3_f64_2026_05_12_v2.tif') - write_geotiff_gpu(da, path, compression='deflate', predictor=3, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=3, + tile_size=16) out = open_geotiff(path) np.testing.assert_array_equal(out.values, arr) @@ -2526,14 +2526,14 @@ def test_predictor_3_rejects_int_dtype(self, tmp_path): with pytest.raises(ValueError, match=r"predictor=3.*requires float"): - write_geotiff_gpu(da, path, compression='deflate', predictor=3, - tile_size=16) + _write_geotiff_gpu(da, path, compression='deflate', predictor=3, + tile_size=16) @requires_gpu class TestWriteGeotiffGpuPredictorCpuParity: """Pixel-exact parity between CPU ``to_geotiff(predictor=X)`` and - GPU ``write_geotiff_gpu(predictor=X)``. + GPU ``_write_geotiff_gpu(predictor=X)``. Predictor encode is a lossless transform: identical inputs must produce identical decoded outputs regardless of whether the @@ -2552,8 +2552,8 @@ def test_cpu_gpu_parity_predictor_2_uint16(self, tmp_path): to_geotiff(_da_with_float_coords(arr), cpu_path, compression='deflate', predictor=2, tile_size=16) - write_geotiff_gpu(_da_with_float_coords(cupy.asarray(arr)), gpu_path, - compression='deflate', predictor=2, tile_size=16) + _write_geotiff_gpu(_da_with_float_coords(cupy.asarray(arr)), gpu_path, + compression='deflate', predictor=2, tile_size=16) cpu_out = open_geotiff(cpu_path).values gpu_out = open_geotiff(gpu_path).values @@ -2570,8 +2570,8 @@ def test_cpu_gpu_parity_predictor_3_float32(self, tmp_path): to_geotiff(_da_with_float_coords(arr), cpu_path, compression='deflate', predictor=3, tile_size=16) - write_geotiff_gpu(_da_with_float_coords(cupy.asarray(arr)), gpu_path, - compression='deflate', predictor=3, tile_size=16) + _write_geotiff_gpu(_da_with_float_coords(cupy.asarray(arr)), gpu_path, + compression='deflate', predictor=3, tile_size=16) cpu_out = open_geotiff(cpu_path).values gpu_out = open_geotiff(gpu_path).values @@ -2604,7 +2604,7 @@ def _make_2x1_mosaic_vrt(tmp_path, left: np.ndarray, """Create a 2x1 horizontal mosaic VRT for cross-source window tests. Hand-built XML so the dst_rect placements are explicit -- VRT's - write_vrt helper only handles single-source layouts directly. + build_vrt helper only handles single-source layouts directly. """ h, lw = left.shape[:2] rw = right.shape[1] @@ -2649,7 +2649,7 @@ def _make_2x1_mosaic_vrt(tmp_path, left: np.ndarray, class TestReadVrtWindowEager: - """Eager numpy ``read_vrt(window=...)`` slices the assembled raster.""" + """Eager numpy ``_read_vrt(window=...)`` slices the assembled raster.""" def test_window_subregion_of_single_source(self, tmp_path): """Window picks a 4x6 sub-block from an 8x16 single-source VRT.""" @@ -2657,7 +2657,7 @@ def test_window_subregion_of_single_source(self, tmp_path): vrt = _make_single_tile_vrt(tmp_path, arr) # rows 2..6, cols 4..10 - result = read_vrt(vrt, window=(2, 4, 6, 10)) + result = _read_vrt(vrt, window=(2, 4, 6, 10)) assert result.shape == (4, 6) np.testing.assert_array_equal(result.values, arr[2:6, 4:10]) @@ -2667,15 +2667,15 @@ def test_window_full_raster_matches_no_window(self, tmp_path): arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - full = read_vrt(vrt).values - windowed = read_vrt(vrt, window=(0, 0, 8, 16)).values + full = _read_vrt(vrt).values + windowed = _read_vrt(vrt, window=(0, 0, 8, 16)).values np.testing.assert_array_equal(windowed, full) def test_window_outside_raster_bounds_rejected(self, tmp_path): """Window extending past raster bounds raises ``ValueError``. - ``read_vrt`` used to silently clamp out-of-bounds windows. That + ``_read_vrt`` used to silently clamp out-of-bounds windows. That masked caller bugs (typo'd coords, off-by-one extents) and made the returned shape disagree with the caller's coord arrays. The validator now rejects such windows up front with a typed @@ -2685,12 +2685,12 @@ def test_window_outside_raster_bounds_rejected(self, tmp_path): vrt = _make_single_tile_vrt(tmp_path, arr) with pytest.raises(ValueError, match="outside the VRT extent"): - read_vrt(vrt, window=(0, 0, 100, 100)) + _read_vrt(vrt, window=(0, 0, 100, 100)) def test_window_negative_offsets_rejected(self, tmp_path): """Negative start offsets raise ``ValueError``. - ``read_vrt`` validates the window + ``_read_vrt`` validates the window against the VRT extent. Negative offsets are rejected the same way an over-large window is, rather than being silently clamped to zero. @@ -2699,7 +2699,7 @@ def test_window_negative_offsets_rejected(self, tmp_path): vrt = _make_single_tile_vrt(tmp_path, arr) with pytest.raises(ValueError, match="outside the VRT extent"): - read_vrt(vrt, window=(-1, -2, 3, 4)) + _read_vrt(vrt, window=(-1, -2, 3, 4)) def test_window_across_mosaic_seam(self, tmp_path): """Window straddling a multi-source seam reads both sources. @@ -2717,7 +2717,7 @@ def test_window_across_mosaic_seam(self, tmp_path): vrt = _make_2x1_mosaic_vrt(tmp_path, left, right) # Window rows 0..4, cols 0..6 (cuts across seam at col 4) - result = read_vrt(vrt, window=(0, 0, 4, 6)) + result = _read_vrt(vrt, window=(0, 0, 4, 6)) assert result.shape == (4, 6) # cols 0-3 of window are cols 0-3 of left @@ -2733,7 +2733,7 @@ def test_window_offset_into_mosaic(self, tmp_path): vrt = _make_2x1_mosaic_vrt(tmp_path, left, right) # Window cols 5..8 -> right cols 1..4 - result = read_vrt(vrt, window=(0, 5, 4, 8)) + result = _read_vrt(vrt, window=(0, 5, 4, 8)) assert result.shape == (4, 3) np.testing.assert_array_equal(result.values, right[:, 1:4]) @@ -2746,13 +2746,13 @@ def test_window_transform_origin_shift(self, tmp_path): must advertise the shifted origin ``origin_x' = origin_x + c0*res_x`` and ``origin_y' = origin_y + r0*res_y``. This is the metadata-propagation contract that ``open_geotiff - (window=)`` already honours; ``read_vrt(window=)`` must + (window=)`` already honours; ``_read_vrt(window=)`` must agree. """ arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - result = read_vrt(vrt, window=(2, 3, 6, 10)) + result = _read_vrt(vrt, window=(2, 3, 6, 10)) # GeoTransform from _vrt.write_vrt default: pixel-is-area, # res_x=1.0, res_y=-1.0, origin (0, 0). @@ -2776,14 +2776,14 @@ def test_window_coords_match_transform_shift(self, tmp_path): arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - result = read_vrt(vrt, window=(2, 3, 6, 10)) + result = _read_vrt(vrt, window=(2, 3, 6, 10)) assert float(result.x[0]) == pytest.approx(3.5) assert float(result.y[0]) == pytest.approx(-2.5) class TestReadVrtWindowWithBand: - """``read_vrt(window=, band=)`` combinations. + """``_read_vrt(window=, band=)`` combinations. A regression in either kwarg's interaction with the other (band selection after window slicing, nodata sentinel resolved per @@ -2795,7 +2795,7 @@ def _make_multiband_vrt(self, tmp_path) -> tuple[str, np.ndarray]: h, w = 4, 8 band0 = np.arange(h * w, dtype=np.float32).reshape(h, w) band1 = (band0 * -1.0).astype(np.float32) - # Stack into 3D so write_vrt produces a multi-band TIFF source + # Stack into 3D so build_vrt produces a multi-band TIFF source full = np.stack([band0, band1], axis=-1) tile_path = str(tmp_path / 'multi.tif') @@ -2809,7 +2809,7 @@ def test_window_plus_band_selection(self, tmp_path): vrt, full = self._make_multiband_vrt(tmp_path) # window rows 1..3, cols 2..6, band 1 - result = read_vrt(vrt, window=(1, 2, 3, 6), band=1) + result = _read_vrt(vrt, window=(1, 2, 3, 6), band=1) assert result.ndim == 2 # band selection yields 2D assert result.shape == (2, 4) @@ -2819,7 +2819,7 @@ def test_window_plus_band_selection(self, tmp_path): class TestReadVrtWindowDask: - """``read_vrt(window=, chunks=)`` returns a dask-chunked DataArray. + """``_read_vrt(window=, chunks=)`` returns a dask-chunked DataArray. The chunk size must apply to the windowed shape, not the full VRT extent. A regression that dropped the window before chunking @@ -2832,7 +2832,7 @@ def test_window_chunks_returns_dask(self, tmp_path): arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - result = read_vrt(vrt, window=(2, 4, 6, 10), chunks=2) + result = _read_vrt(vrt, window=(2, 4, 6, 10), chunks=2) assert isinstance(result.data, da_mod.Array) assert result.shape == (4, 6) @@ -2843,7 +2843,7 @@ def test_window_chunks_returns_dask(self, tmp_path): @requires_gpu class TestReadVrtWindowGpu: - """``read_vrt(window=, gpu=True)`` returns a CuPy-backed DataArray. + """``_read_vrt(window=, gpu=True)`` returns a CuPy-backed DataArray. The eager VRT decode happens on CPU (the internal reader walks SimpleSources and assembles); the final ``if gpu: cupy.asarray`` @@ -2858,7 +2858,7 @@ def test_window_gpu_returns_cupy(self, tmp_path): arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - result = read_vrt(vrt, window=(2, 4, 6, 10), gpu=True) + result = _read_vrt(vrt, window=(2, 4, 6, 10), gpu=True) assert isinstance(result.data, cupy.ndarray) assert result.shape == (4, 6) @@ -2874,7 +2874,7 @@ def test_window_gpu_chunks_returns_dask_cupy(self, tmp_path): arr = np.arange(8 * 16, dtype=np.float32).reshape(8, 16) vrt = _make_single_tile_vrt(tmp_path, arr) - result = read_vrt(vrt, window=(2, 4, 6, 10), gpu=True, chunks=2) + result = _read_vrt(vrt, window=(2, 4, 6, 10), gpu=True, chunks=2) assert isinstance(result.data, da_mod.Array) assert isinstance(result.data._meta, cupy.ndarray) @@ -2995,7 +2995,7 @@ def test_vrt_missing_source_default_warns_then_continues( clear_strict_env, tmp_path, ): """A VRT referencing a missing source file warns once and skips it.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path = tmp_path / 'mosaic_1662_missing.vrt' missing_src = f'{tmp_path}/does_not_exist_1662.tif' @@ -3018,10 +3018,10 @@ def test_vrt_missing_source_default_warns_then_continues( with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - # Public ``read_vrt`` defaults to ``missing_sources='raise'`` + # Public ``_read_vrt`` defaults to ``missing_sources='raise'`` # since #1860. Opt back into the lenient warn-then-continue # behaviour to keep exercising the warning path. - da = read_vrt(str(vrt_path), missing_sources='warn') + da = _read_vrt(str(vrt_path), missing_sources='warn') # The mosaic should still load (with a hole) and one warning should # describe the skipped source. @@ -3037,7 +3037,7 @@ def test_vrt_missing_source_default_warns_then_continues( def test_vrt_missing_source_strict_raises(set_strict_env, tmp_path): """In strict mode the missing source surfaces as an exception.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt vrt_path = tmp_path / 'mosaic_1662_missing_strict.vrt' missing_src = f'{tmp_path}/does_not_exist_1662_strict.tif' @@ -3059,7 +3059,7 @@ def test_vrt_missing_source_strict_raises(set_strict_env, tmp_path): ) with pytest.raises(Exception): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) # --------------------------------------------------------------------------- @@ -3142,7 +3142,7 @@ def site(): # --------------------------------------------------------------------------- -# read_geotiff_gpu on_gpu_failure='auto' + env var integration +# _read_geotiff_gpu on_gpu_failure='auto' + env var integration # --------------------------------------------------------------------------- def _gpu_available() -> bool: @@ -3160,13 +3160,13 @@ def _gpu_available() -> bool: @pytest.mark.skipif( not _HAS_GPU, - reason="cupy + CUDA required for read_geotiff_gpu fallback test", + reason="cupy + CUDA required for _read_geotiff_gpu fallback test", ) def test_read_geotiff_gpu_env_var_promotes_to_strict(monkeypatch, tmp_path): """With on_gpu_failure='auto' but XRSPATIAL_GEOTIFF_STRICT=1, a GPU decode failure surfaces instead of falling back to CPU. - The seam: ``read_geotiff_gpu`` does a local + The seam: ``_read_geotiff_gpu`` does a local ``from ._gpu_decode import gpu_decode_tiles_from_file`` inside the function body, so rebinding the attribute on the ``xrspatial.geotiff._gpu_decode`` module is picked up on the next @@ -3177,7 +3177,7 @@ def test_read_geotiff_gpu_env_var_promotes_to_strict(monkeypatch, tmp_path): import numpy as np import xarray as xr - from xrspatial.geotiff import _gpu_decode, read_geotiff_gpu, to_geotiff + from xrspatial.geotiff import _gpu_decode, _read_geotiff_gpu, to_geotiff # 1. Write a small valid TIF so the metadata parse succeeds and we # reach the GPU decode stage. @@ -3205,7 +3205,7 @@ def _boom(*args, **kwargs): monkeypatch.delenv('XRSPATIAL_GEOTIFF_STRICT', raising=False) with warnings.catch_warnings(): warnings.simplefilter('ignore') - result = read_geotiff_gpu(p) + result = _read_geotiff_gpu(p) assert isinstance(result, xr.DataArray) assert isinstance(result.data, cp.ndarray) @@ -3213,7 +3213,7 @@ def _boom(*args, **kwargs): # the patched RuntimeError instead of falling back. monkeypatch.setenv('XRSPATIAL_GEOTIFF_STRICT', '1') with pytest.raises(RuntimeError, match=sentinel): - read_geotiff_gpu(p) + _read_geotiff_gpu(p) # =========================================================================== # Public namespace no-leak regression (#1708) diff --git a/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py b/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py index ab7b4159e..1eebc6270 100644 --- a/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py +++ b/xrspatial/geotiff/tests/vrt/test_dtype_conversion.py @@ -24,7 +24,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import read_vrt, to_geotiff +from xrspatial.geotiff import _read_vrt, to_geotiff from xrspatial.geotiff._errors import MixedBandMetadataError, VRTUnsupportedError from xrspatial.geotiff._vrt import (_NP_TO_VRT_DTYPE, _parse_band_nodata, _vrt_dtype_name_for, parse_vrt) @@ -78,7 +78,7 @@ def test_complex_dtype_raises_value_error(tmp_path, cdtype): _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr=cdtype, src_path=str(src)) with pytest.raises(ValueError) as ei: - read_vrt(vrt) + _read_vrt(vrt) msg = str(ei.value) assert cdtype in msg, f'error message must name {cdtype!r}: {msg!r}' assert 'band=1' in msg or 'band 1' in msg, f'error message must name the band: {msg!r}' @@ -94,7 +94,7 @@ def test_garbage_dtype_raises_value_error(tmp_path): _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Garbage', src_path=str(src)) with pytest.raises(ValueError, match='Garbage'): - read_vrt(vrt) + _read_vrt(vrt) def test_typo_for_supported_dtype_is_still_rejected(tmp_path): @@ -107,7 +107,7 @@ def test_typo_for_supported_dtype_is_still_rejected(tmp_path): _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Flaot32', src_path=str(src)) with pytest.raises(ValueError, match='Flaot32'): - read_vrt(vrt) + _read_vrt(vrt) def test_uint64_round_trip(tmp_path): @@ -121,7 +121,7 @@ def test_uint64_round_trip(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='UInt64', src_path=str(src)) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.uint64, f'UInt64 VRT must read as uint64; got {r.dtype}' np.testing.assert_array_equal(r.values, b) assert int(r.values[1, 1]) == big @@ -138,7 +138,7 @@ def test_int64_round_trip(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Int64', src_path=str(src)) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.int64, f'Int64 VRT must read as int64; got {r.dtype}' np.testing.assert_array_equal(r.values, b) @@ -154,7 +154,7 @@ def test_missing_dtype_attribute_defaults_to_float32(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='', src_path=str(src)) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32, f'missing dataType must default to Float32; got {r.dtype}' np.testing.assert_allclose(r.values, b) @@ -168,7 +168,7 @@ def test_byte_dtype_still_works(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Byte', src_path=str(src)) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.uint8 np.testing.assert_array_equal(r.values, b) @@ -181,7 +181,7 @@ def test_float64_dtype_still_works(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Float64', src_path=str(src)) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64 np.testing.assert_allclose(r.values, b) @@ -310,7 +310,7 @@ def test_uint64_nodata_round_trip_preserves_max_sentinel(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='UInt64', src_path=str(src), nodata=big) # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert 'nodata' in r.attrs assert int(r.attrs['nodata']) == big assert isinstance(r.attrs['nodata'], (int, np.integer)) @@ -329,7 +329,7 @@ def test_uint64_nodata_masks_max_sentinel_in_data(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='UInt64', src_path=str(src), nodata=big) # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64, f'sentinel hit must promote to float64, got {r.dtype}' assert np.isnan(r.values[1, 1]), f'the 2**64-1 cell must be masked to NaN; got {r.values[1, 1]!r}' # noqa: E501 assert r.values[0, 0] == 1.0 @@ -347,7 +347,7 @@ def test_int64_min_nodata_masks_correctly(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Int64', src_path=str(src), nodata=info.min) # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64 assert np.isnan(r.values[0, 0]) assert r.values[0, 1] == -1.0 @@ -366,7 +366,7 @@ def test_int32_negative_nodata_still_masks(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Int32', src_path=str(src), nodata=-9999) # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64 assert np.isnan(r.values[0, 1]) assert np.isnan(r.values[1, 0]) @@ -383,7 +383,7 @@ def test_float32_nan_nodata_still_works(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Float32', src_path=str(src), nodata='nan') # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32 assert np.isnan(r.attrs['nodata']) assert np.isnan(r.values[0, 1]) @@ -397,7 +397,7 @@ def test_float64_scientific_nodata_still_works(tmp_path): src = tmp_path / 'src.tif' _dtype_validation_write(b, src) vrt = _dtype_validation_build_single_band_vrt(tmp_path, dtype_attr='Float64', src_path=str(src), nodata='-1.5e10') # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64 assert r.attrs['nodata'] == -15000000000.0 @@ -509,7 +509,7 @@ def test_float32_vrt_uint16_source_masks_in_range_sentinel(tmp_path): """ src = _int_source_float_dtype_write_uint16_with_sentinel(tmp_path) vrt = _int_source_float_dtype_build_vrt(tmp_path, src, 'Float32', 65535) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32, f'Float32-declared VRT should return float32, got {r.dtype}' assert np.isnan(r.values[1, 1]), f'Sentinel pixel (uint16 65535 -> float32) should be NaN-masked; got values[1, 1]={r.values[1, 1]}' # noqa: E501 assert r.attrs.get('nodata') == 65535.0 @@ -520,7 +520,7 @@ def test_float64_vrt_int16_source_masks_negative_sentinel(tmp_path): """Float64 VRT, int16 source with negative sentinel: pixel becomes NaN.""" src = _int_source_float_dtype_write_int16_with_sentinel(tmp_path, sentinel=-1) vrt = _int_source_float_dtype_build_vrt(tmp_path, src, 'Float64', -1) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float64 assert np.isnan(r.values[1, 1]), f'Sentinel pixel (-1) should be NaN-masked; got values[1, 1]={r.values[1, 1]}' # noqa: E501 assert r.attrs.get('nodata') == -1.0 @@ -537,7 +537,7 @@ def test_float32_vrt_out_of_range_sentinel_is_noop(tmp_path): p = str(tmp_path / 'b0_no_nodata.tif') write(arr, p, compression='none', tiled=False) vrt = _int_source_float_dtype_build_vrt(tmp_path, p, 'Float32', -9999) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32 assert not np.isnan(r.values).any() assert r.attrs.get('nodata') == -9999.0 @@ -555,7 +555,7 @@ def test_float32_vrt_uint16_source_no_sentinel_pixels(tmp_path): p = str(tmp_path / 'b0_clean.tif') write(arr, p, compression='none', tiled=False) vrt = _int_source_float_dtype_build_vrt(tmp_path, p, 'Float32', 65535) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32 assert not np.isnan(r.values).any() np.testing.assert_array_equal(r.values, arr.astype(np.float32)) @@ -568,7 +568,7 @@ def test_float_vrt_int_source_dask_path_masks_sentinel(tmp_path): """ src = _int_source_float_dtype_write_uint16_with_sentinel(tmp_path) vrt = _int_source_float_dtype_build_vrt(tmp_path, src, 'Float32', 65535) - r = read_vrt(vrt, chunks=2) + r = _read_vrt(vrt, chunks=2) assert r.dtype == np.float32 val = r.values assert np.isnan(val[1, 1]) @@ -581,7 +581,7 @@ def test_float_vrt_int_source_round_trip_nodata_attr(tmp_path): """ src = _int_source_float_dtype_write_uint16_with_sentinel(tmp_path) vrt = _int_source_float_dtype_build_vrt(tmp_path, src, 'Float32', 65535) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.attrs.get('nodata') == 65535.0 @@ -596,11 +596,11 @@ def test_float_vrt_int_source_with_band_select(tmp_path): vrt_path = str(tmp_path / 'mb.vrt') with open(vrt_path, 'w') as f: f.write(vrt_xml) - r0 = read_vrt(vrt_path, band=0, band_nodata='first') + r0 = _read_vrt(vrt_path, band=0, band_nodata='first') assert r0.dtype == np.float32 assert np.isnan(r0.values[1, 1]) assert r0.attrs.get('nodata') == 65535.0 - r1 = read_vrt(vrt_path, band=1, band_nodata='first') + r1 = _read_vrt(vrt_path, band=1, band_nodata='first') assert r1.dtype == np.float32 assert np.isnan(r1.values[1, 1]) assert r1.attrs.get('nodata') == 65000.0 @@ -662,7 +662,7 @@ def test_mixed_byte_and_float32_bands_raise(tmp_path): _multiband_dtype_write(b1, p1) vrt_path = _multiband_dtype_build_two_band_vrt(tmp_path, b0_dtype_str='Byte', b0_path=str(p0), b1_dtype_str='Float32', b1_path=str(p1)) # noqa: E501 with pytest.raises(MixedBandMetadataError) as excinfo: - read_vrt(vrt_path) + _read_vrt(vrt_path) msg = str(excinfo.value).lower() assert 'band 1' in msg and 'band 2' in msg @@ -679,7 +679,7 @@ def test_complex_source_scale_promotes_buffer_to_float(tmp_path): _multiband_dtype_write(b, p0) _multiband_dtype_write(b, p1) vrt_path = _multiband_dtype_build_complex_source_vrt(tmp_path, dtype_str='Byte', src_path=str(p1), scale_ratio=0.5, other_band_dtype='Byte', other_band_path=str(p0)) # noqa: E501 - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.dtype.kind == 'f', f'ScaleRatio on a Byte band must widen the buffer to float; got {r.dtype}' # noqa: E501 expected = b.astype(np.float64) * 0.5 np.testing.assert_allclose(r.values[..., 1], expected) @@ -698,7 +698,7 @@ def test_all_byte_no_scaling_stays_uint8(tmp_path): _multiband_dtype_write(b0, p0) _multiband_dtype_write(b1, p1) vrt_path = _multiband_dtype_build_two_band_vrt(tmp_path, b0_dtype_str='Byte', b0_path=str(p0), b1_dtype_str='Byte', b1_path=str(p1)) # noqa: E501 - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.dtype == np.uint8, f'All-Byte VRT with no scaling must stay uint8; got {r.dtype}' np.testing.assert_array_equal(r.values[..., 0], b0) np.testing.assert_array_equal(r.values[..., 1], b1) @@ -712,7 +712,7 @@ def test_complex_source_scale_and_offset_preserve_precision(tmp_path): Note: the ``ComplexSource`` branch of ``parse_vrt`` in ``_vrt.py`` maps the XML ```` to the dataclass ``scale`` attribute and ```` to the ``offset`` attribute, then the - ``# Apply ComplexSource scaling`` block in ``read_vrt`` applies + ``# Apply ComplexSource scaling`` block in ``_read_vrt`` applies ``src_arr = src_arr * scale + offset``. """ b = np.array([[10, 11], [12, 13]], dtype=np.uint8) @@ -721,7 +721,7 @@ def test_complex_source_scale_and_offset_preserve_precision(tmp_path): _multiband_dtype_write(b, p0) _multiband_dtype_write(b, p1) vrt_path = _multiband_dtype_build_complex_source_vrt(tmp_path, dtype_str='Byte', src_path=str(p1), scale_ratio=0.25, scale_offset=1.5, other_band_dtype='Byte', other_band_path=str(p0)) # noqa: E501 - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.dtype.kind == 'f' expected = b.astype(np.float64) * 0.25 + 1.5 np.testing.assert_allclose(r.values[..., 1], expected) @@ -747,14 +747,14 @@ def test_mixed_byte_and_int16_bands_raise(tmp_path): out = tmp_path / 'mixed.vrt' out.write_text(vrt_path) with pytest.raises(MixedBandMetadataError) as excinfo: - read_vrt(str(out), band_nodata='first') + _read_vrt(str(out), band_nodata='first') msg = str(excinfo.value).lower() assert 'band 1' in msg and 'band 2' in msg def test_single_band_complex_source_scale_widens_buffer(tmp_path): """Single-band ``Byte`` VRT with ``0.5``. - The single-band branch in ``read_vrt`` must mirror the multi-band + The single-band branch in ``_read_vrt`` must mirror the multi-band widening logic; previously it used ``selected_bands[0].dtype`` directly, so the scaled source values truncated back to uint8. """ @@ -762,7 +762,7 @@ def test_single_band_complex_source_scale_widens_buffer(tmp_path): p = tmp_path / 'b.tif' _multiband_dtype_write(b, p) vrt_path = _multiband_dtype_build_complex_source_vrt(tmp_path, dtype_str='Byte', src_path=str(p), scale_ratio=0.5, extra_band=False) # noqa: E501 - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.ndim == 2, f'Single-band VRT must return a 2D array; got shape {r.shape}' assert r.dtype.kind == 'f', f'Single-band scaled VRT must widen to float; got {r.dtype}' expected = b.astype(np.float64) * 0.5 @@ -784,7 +784,7 @@ def test_band_select_uint8_first_then_float_returns_float_for_band_1(tmp_path): _multiband_dtype_write(b0, p0) _multiband_dtype_write(b1, p1) vrt_path = _multiband_dtype_build_two_band_vrt(tmp_path, b0_dtype_str='Byte', b0_path=str(p0), b1_dtype_str='Float32', b1_path=str(p1)) # noqa: E501 - r = read_vrt(vrt_path, band=1) + r = _read_vrt(vrt_path, band=1) assert r.dtype == np.float32 np.testing.assert_allclose(r.values, b1) @@ -800,7 +800,7 @@ def test_band_select_uint8_first_then_float_returns_uint8_for_band_0(tmp_path): _multiband_dtype_write(b0, p0) _multiband_dtype_write(b1, p1) vrt_path = _multiband_dtype_build_two_band_vrt(tmp_path, b0_dtype_str='Byte', b0_path=str(p0), b1_dtype_str='Float32', b1_path=str(p1)) # noqa: E501 - r = read_vrt(vrt_path, band=0) + r = _read_vrt(vrt_path, band=0) assert r.dtype == np.uint8 np.testing.assert_array_equal(r.values, b0) @@ -817,7 +817,7 @@ def test_all_float32_multiband_stays_float32(tmp_path): _multiband_dtype_write(b0, p0) _multiband_dtype_write(b1, p1) vrt_path = _multiband_dtype_build_two_band_vrt(tmp_path, b0_dtype_str='Float32', b0_path=str(p0), b1_dtype_str='Float32', b1_path=str(p1)) # noqa: E501 - r = read_vrt(vrt_path) + r = _read_vrt(vrt_path) assert r.dtype == np.float32 np.testing.assert_allclose(r.values[..., 0], b0) np.testing.assert_allclose(r.values[..., 1], b1) @@ -825,7 +825,7 @@ def test_all_float32_multiband_stays_float32(tmp_path): def test_zero_band_vrt_raises_value_error(tmp_path): """A malformed VRT with zero ```` children must - surface a clear ``ValueError`` from ``read_vrt`` rather than the + surface a clear ``ValueError`` from ``_read_vrt`` rather than the generic ``"at least one array or dtype is required"`` message raised by ``np.result_type`` when called with no arguments. """ @@ -834,7 +834,7 @@ def test_zero_band_vrt_raises_value_error(tmp_path): p = tmp_path / 'empty.vrt' p.write_text(vrt_xml) with pytest.raises(ValueError, match='no '): - read_vrt(str(p)) + _read_vrt(str(p)) # --------------------------------------------------------------------------- @@ -869,7 +869,7 @@ def test_multiband_uint16_per_band_sentinel_each_masked(tmp_path): as NaN but band 1's (1,1) cell as the literal 65000.0. """ vrt_path = _multiband_int_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.shape == (2, 2, 2) assert r.dtype == np.float64, f'expected float64 promotion, got {r.dtype}' assert np.isnan(r.values[1, 1, 0]), "band 0's sentinel pixel was not NaN-masked." @@ -889,7 +889,7 @@ def test_multiband_int32_negative_per_band_sentinel(tmp_path): range guard accepts negatives. """ vrt_path = _multiband_int_nodata_write_two_band_per_band_nodata_vrt(tmp_path, dtype_str='Int32', np_dtype=np.int32, band0_sentinel=-9999, band1_sentinel=-7777, band0_other=(10, 20, 30), band1_other=(40, 50, 60)) # noqa: E501 - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.dtype == np.float64 assert np.isnan(r.values[1, 1, 0]) assert np.isnan(r.values[1, 1, 1]) @@ -909,7 +909,7 @@ def test_multiband_only_one_band_has_sentinel_present(tmp_path): import os p1 = os.path.join(os.path.dirname(vrt_path), 'vrt_b1_1611.tif') write(b1_no_sentinel, p1, nodata=65000, compression='none', tiled=False) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.dtype == np.float64, "Even when only band 0 has a present sentinel, the array still needs promotion so band 0's NaN can be expressed." # noqa: E501 assert np.isnan(r.values[1, 1, 0]) assert r.values[1, 1, 1] == 99.0 @@ -927,7 +927,7 @@ def test_multiband_no_sentinel_present_anywhere_keeps_int_dtype(tmp_path): p1 = os.path.join(os.path.dirname(vrt_path), 'vrt_b1_1611.tif') write(b0, p0, nodata=65535, compression='none', tiled=False) write(b1, p1, nodata=65000, compression='none', tiled=False) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.dtype == np.uint16 assert r.values[1, 1, 0] == 4 assert r.values[1, 1, 1] == 10 @@ -944,7 +944,7 @@ def test_multiband_per_band_out_of_range_sentinel_is_no_op(tmp_path): xml = xml.replace('10', '-9999') with open(vrt_path, 'w') as f: f.write(xml) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert np.isnan(r.values[1, 1, 0]) assert r.values[1, 1, 1] == 10.0 or r.values[1, 1, 1] == 10 @@ -957,8 +957,8 @@ def test_multiband_band_kwarg_still_per_band_post_pr1602(tmp_path): sentinel. """ vrt_path = _multiband_int_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r0 = read_vrt(vrt_path, band=0, band_nodata='first') - r1 = read_vrt(vrt_path, band=1, band_nodata='first') + r0 = _read_vrt(vrt_path, band=0, band_nodata='first') + r1 = _read_vrt(vrt_path, band=1, band_nodata='first') assert r0.dtype == np.float64 assert r1.dtype == np.float64 assert r0.attrs.get('nodata') == 65535.0 @@ -973,7 +973,7 @@ def test_multiband_attrs_nodata_still_band0(tmp_path): The pixel-level fix must not change that contract. """ vrt_path = _multiband_int_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.attrs.get('nodata') == 65535.0 @@ -1234,7 +1234,7 @@ def test_eager_2x1_mosaic_values_coords_attrs(simple_mosaic_mosaic_2x1): / nodata on attrs. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_2x1 - result = read_vrt(vrt_path) + result = _read_vrt(vrt_path) assert result.shape == expected.shape, f'eager 2x1 shape {result.shape}, expected {expected.shape}' # noqa: E501 np.testing.assert_array_equal(result.values, expected) _simple_mosaic_assert_coords_monotonic(result, expected_origin_x=ox, expected_origin_y=oy) @@ -1249,7 +1249,7 @@ def test_eager_2x2_mosaic_values_coords_attrs(simple_mosaic_mosaic_2x2): only as a numeric diff. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_2x2 - result = read_vrt(vrt_path) + result = _read_vrt(vrt_path) assert result.shape == expected.shape, f'eager 2x2 shape {result.shape}, expected {expected.shape}' # noqa: E501 np.testing.assert_array_equal(result.values, expected) _simple_mosaic_assert_coords_monotonic(result, expected_origin_x=ox, expected_origin_y=oy) @@ -1268,9 +1268,9 @@ def test_windowed_read_aligned_with_source_boundary(simple_mosaic_mosaic_2x1): vrt_path, expected, ox, oy = simple_mosaic_mosaic_2x1 h = expected.shape[0] r0, c0, r1, c1 = (0, 16, h, 48) - result = read_vrt(vrt_path, window=(r0, c0, r1, c1)) + result = _read_vrt(vrt_path, window=(r0, c0, r1, c1)) np.testing.assert_array_equal(result.values, expected[r0:r1, c0:c1]) - full = read_vrt(vrt_path) + full = _read_vrt(vrt_path) np.testing.assert_array_equal(np.asarray(result['x'].values), np.asarray(full['x'].values)[c0:c1]) # noqa: E501 np.testing.assert_array_equal(np.asarray(result['y'].values), np.asarray(full['y'].values)[r0:r1]) # noqa: E501 expected_window_ox = ox + _PIXEL_W * c0 @@ -1283,7 +1283,7 @@ def test_dask_2x1_mosaic_multi_chunk_matches_eager(simple_mosaic_mosaic_2x1): pixels as the eager read, and uses a real multi-block dask graph. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_2x1 - chunked = read_vrt(vrt_path, chunks=(16, 16)) + chunked = _read_vrt(vrt_path, chunks=(16, 16)) assert isinstance(chunked.data, da.Array), f'expected dask Array, got {type(chunked.data).__name__}' # noqa: E501 assert chunked.data.numblocks == (2, 4), f'expected 2x4 blocks, got {chunked.data.numblocks}' computed = chunked.compute() @@ -1299,7 +1299,7 @@ def test_dask_2x2_mosaic_multi_chunk_matches_eager(simple_mosaic_mosaic_2x2): mosaic is 64x64 so the resulting dask array is 4x4 blocks. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_2x2 - chunked = read_vrt(vrt_path, chunks=(16, 16)) + chunked = _read_vrt(vrt_path, chunks=(16, 16)) assert isinstance(chunked.data, da.Array) assert chunked.data.numblocks == (4, 4), f'expected 4x4 blocks, got {chunked.data.numblocks}' computed = chunked.compute() @@ -1317,7 +1317,7 @@ def test_eager_multiband_2x1_mosaic(simple_mosaic_mosaic_multiband_2x1): the fixture. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_multiband_2x1 - result = read_vrt(vrt_path) + result = _read_vrt(vrt_path) assert result.shape == expected.shape, f'multiband 2x1 shape {result.shape}, expected {expected.shape}' # noqa: E501 np.testing.assert_array_equal(result.values, expected) _simple_mosaic_assert_coords_monotonic(result, expected_origin_x=ox, expected_origin_y=oy) @@ -1333,8 +1333,8 @@ def test_dask_multiband_2x1_mosaic_matches_eager(simple_mosaic_mosaic_multiband_ test above. """ vrt_path, expected, ox, oy = simple_mosaic_mosaic_multiband_2x1 - eager = read_vrt(vrt_path) - chunked = read_vrt(vrt_path, chunks=(16, 16)) + eager = _read_vrt(vrt_path) + chunked = _read_vrt(vrt_path, chunks=(16, 16)) assert isinstance(chunked.data, da.Array), f'expected dask Array, got {type(chunked.data).__name__}' # noqa: E501 computed = chunked.compute() assert computed.shape == eager.shape diff --git a/xrspatial/geotiff/tests/vrt/test_metadata.py b/xrspatial/geotiff/tests/vrt/test_metadata.py index 4c5a30c28..ae806c35c 100644 --- a/xrspatial/geotiff/tests/vrt/test_metadata.py +++ b/xrspatial/geotiff/tests/vrt/test_metadata.py @@ -8,13 +8,13 @@ * ``masked_nodata`` attr honours ``mask_nodata`` kwarg * Per-band ```` selection * SimpleSource ``0`` survives the falsy-zero bug -* Integer-with-nodata promotion through ``read_vrt`` +* Integer-with-nodata promotion through ``_read_vrt`` * ``mask_nodata=False`` preserves float sentinels * Tile-level metadata parity for VRT tiled writes * VRT XML parsed once on the chunked path -* ``write_vrt`` escapes XML special characters -* XML size cap on eager ``read_vrt`` -* XML size cap on chunked ``read_vrt`` +* ``build_vrt`` escapes XML special characters +* XML size cap on eager ``_read_vrt`` +* XML size cap on chunked ``_read_vrt`` * VRT metadata parity across backends """ from __future__ import annotations @@ -32,7 +32,7 @@ import xarray as xr from xrspatial.geotiff import (GeoTIFFFallbackWarning, MixedBandMetadataError, open_geotiff, - read_geotiff_dask, read_vrt, to_geotiff, write_vrt) + _read_geotiff_dask, _read_vrt, to_geotiff, build_vrt) from xrspatial.geotiff._attrs import GEOREF_STATUS_FULL, GEOREF_STATUS_TRANSFORM_ONLY from xrspatial.geotiff._errors import VRTUnsupportedError from xrspatial.geotiff._geotags import GeoTransform @@ -99,7 +99,7 @@ def test_skipped_source_records_vrt_holes_attr(holes_attr_clear_strict_env, tmp_ _holes_attr_write_vrt_with_missing_source(vrt_path, missing_src) with warnings.catch_warnings(): warnings.simplefilter('ignore', GeoTIFFFallbackWarning) - da = read_vrt(str(vrt_path), missing_sources='warn') + da = _read_vrt(str(vrt_path), missing_sources='warn') assert np.issubdtype(da.dtype, np.integer) assert (da.values == 0).all() assert 'vrt_holes' in da.attrs @@ -143,7 +143,7 @@ def test_no_holes_attr_when_all_sources_read(holes_attr_clear_strict_env, tmp_pa ) with warnings.catch_warnings(): warnings.simplefilter('error', GeoTIFFFallbackWarning) - da = read_vrt(str(vrt_path)) + da = _read_vrt(str(vrt_path)) assert 'vrt_holes' not in da.attrs @@ -161,7 +161,7 @@ def test_strict_mode_still_raises(holes_attr_set_strict_env, tmp_path): missing_src = f'{tmp_path}/does_not_exist_1734_strict.tif' _holes_attr_write_vrt_with_missing_source(vrt_path, missing_src) with pytest.raises(FileNotFoundError, match='does_not_exist_1734_strict.tif'): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) def test_warning_mentions_how_to_detect_holes(holes_attr_clear_strict_env, tmp_path): @@ -173,7 +173,7 @@ def test_warning_mentions_how_to_detect_holes(holes_attr_clear_strict_env, tmp_p _holes_attr_write_vrt_with_missing_source(vrt_path, missing_src) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - read_vrt(str(vrt_path), missing_sources='warn') + _read_vrt(str(vrt_path), missing_sources='warn') fallback = [x for x in w if issubclass(x.category, GeoTIFFFallbackWarning)] assert fallback, 'expected at least one GeoTIFFFallbackWarning' msg = ' '.join((str(x.message) for x in fallback)) @@ -260,7 +260,7 @@ def test_vrt_chunked_float_source_mask_off_reports_false(tmp_path): """Chunked VRT path (``chunks=`` triggers ``_read_vrt_chunked``) + float source + ``mask_nodata=False`` must report False.""" vrt = _masked_nodata_attr_write_float_vrt(tmp_path, 'tmp_2159_chunked_float_src.tif', 'tmp_2159_chunked_unmasked.vrt') # noqa: E501 - out = read_geotiff_dask(vrt, chunks=2, mask_nodata=False) + out = _read_geotiff_dask(vrt, chunks=2, mask_nodata=False) assert out.attrs.get('nodata') == -9999.0 assert out.attrs.get('masked_nodata') is False, f"chunked VRT path: caller opted out of masking but attrs say masked_nodata = {out.attrs.get('masked_nodata')!r}" # noqa: E501 @@ -268,7 +268,7 @@ def test_vrt_chunked_float_source_mask_off_reports_false(tmp_path): def test_vrt_chunked_float_source_mask_on_reports_true(tmp_path): """Canonical direction on the chunked path: masking on, attr True.""" vrt = _masked_nodata_attr_write_float_vrt(tmp_path, 'tmp_2159_chunked_float_src_masked.tif', 'tmp_2159_chunked_masked.vrt') # noqa: E501 - out = read_geotiff_dask(vrt, chunks=2) + out = _read_geotiff_dask(vrt, chunks=2) assert out.attrs.get('nodata') == -9999.0 assert out.attrs.get('masked_nodata') is True @@ -279,7 +279,7 @@ def test_vrt_chunked_int_source_mask_off_reports_false(tmp_path): earlier in the function is itself gated on ``mask_nodata``. The attr says False under both the old and the new rule.""" vrt = _masked_nodata_attr_write_int_vrt(tmp_path, 'tmp_2159_chunked_int_src.tif', 'tmp_2159_chunked_int_unmasked.vrt') # noqa: E501 - out = read_geotiff_dask(vrt, chunks=2, mask_nodata=False) + out = _read_geotiff_dask(vrt, chunks=2, mask_nodata=False) assert out.dtype.kind == 'i' assert out.attrs.get('masked_nodata') is False @@ -289,7 +289,7 @@ def test_vrt_chunked_float_source_mask_off_with_cast_reports_false(tmp_path): cast. Same logic as the eager equivalent: caller opted out of masking, attr is False even though the lazy graph dtype is float.""" vrt = _masked_nodata_attr_write_float_vrt(tmp_path, 'tmp_2159_chunked_float_src_cast.tif', 'tmp_2159_chunked_unmasked_cast.vrt') # noqa: E501 - out = read_geotiff_dask(vrt, chunks=2, mask_nodata=False, dtype=np.float64) + out = _read_geotiff_dask(vrt, chunks=2, mask_nodata=False, dtype=np.float64) assert out.dtype == np.float64 assert out.attrs.get('masked_nodata') is False assert out.attrs.get('nodata_dtype_cast') == 'float64' @@ -302,7 +302,7 @@ def test_vrt_attr_matches_dask_backend_under_mask_off(tmp_path): ``_attrs._set_nodata_attrs`` calls out.""" vrt = _masked_nodata_attr_write_float_vrt(tmp_path, 'tmp_2159_xbackend_src.tif', 'tmp_2159_xbackend.vrt') # noqa: E501 eager = open_geotiff(vrt, mask_nodata=False, dtype=np.float64) - chunked = read_geotiff_dask(vrt, chunks=2, mask_nodata=False, dtype=np.float64) + chunked = _read_geotiff_dask(vrt, chunks=2, mask_nodata=False, dtype=np.float64) assert eager.attrs.get('masked_nodata') is False assert chunked.attrs.get('masked_nodata') is False assert eager.attrs.get('masked_nodata') == chunked.attrs.get('masked_nodata') @@ -342,7 +342,7 @@ def test_read_vrt_band0_uses_band0_nodata(tmp_path): at the call site that the test is exercising the legacy behaviour. """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band=0, band_nodata='first') + r = _read_vrt(vrt_path, band=0, band_nodata='first') assert r.dtype == np.float64 assert r.attrs.get('nodata') == 65535.0 assert np.isnan(r.values[1, 1]) @@ -356,7 +356,7 @@ def test_read_vrt_band1_uses_band1_nodata(tmp_path): [9,65000]] and attrs['nodata']=65535. """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band=1, band_nodata='first') + r = _read_vrt(vrt_path, band=1, band_nodata='first') assert r.dtype == np.float64, 'band=1 read kept uint16 dtype; per-band nodata regression.' assert r.attrs.get('nodata') == 65000.0, f"attrs['nodata'] was {r.attrs.get('nodata')}, expected 65000 from band 1's ." # noqa: E501 assert np.isnan(r.values[1, 1]), "band 1's sentinel pixel was not NaN-masked; promotion ran against the wrong sentinel." # noqa: E501 @@ -373,7 +373,7 @@ def test_read_vrt_no_band_keeps_band0_nodata_attr(tmp_path): "first band wins" contract for multi-band reads. """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band_nodata='first') + r = _read_vrt(vrt_path, band_nodata='first') assert r.attrs.get('nodata') == 65535.0 @@ -385,7 +385,7 @@ def test_read_vrt_negative_band_raises(tmp_path): """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) with pytest.raises(ValueError, match='band'): - read_vrt(vrt_path, band=-1) + _read_vrt(vrt_path, band=-1) def test_read_vrt_out_of_range_band_raises(tmp_path): @@ -395,7 +395,7 @@ def test_read_vrt_out_of_range_band_raises(tmp_path): """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) with pytest.raises(ValueError, match='out of range'): - read_vrt(vrt_path, band=5, band_nodata='first') + _read_vrt(vrt_path, band=5, band_nodata='first') def test_read_vrt_non_integer_band_raises(tmp_path): @@ -405,9 +405,9 @@ def test_read_vrt_non_integer_band_raises(tmp_path): """ vrt_path = _band_nodata_write_two_band_per_band_nodata_vrt(tmp_path) with pytest.raises(ValueError, match='band'): - read_vrt(vrt_path, band='1') + _read_vrt(vrt_path, band='1') with pytest.raises(ValueError, match='band'): - read_vrt(vrt_path, band=True) + _read_vrt(vrt_path, band=True) # --------------------------------------------------------------------------- @@ -508,8 +508,8 @@ def test_vrt_uint16_nodata_promotes_to_float64(tmp_path): assert eager.dtype == np.float64 assert np.isnan(eager.values[1, 0]) vrt_path = str(tmp_path / 'src_1564.vrt') - write_vrt(vrt_path, [tif]) - via_vrt = read_vrt(vrt_path) + build_vrt(vrt_path, [tif]) + via_vrt = _read_vrt(vrt_path) assert via_vrt.dtype == np.float64, f'VRT integer-with-nodata should promote to float64; got {via_vrt.dtype}' # noqa: E501 assert np.isnan(via_vrt.values[1, 0]), f'VRT sentinel pixel should be NaN; got {via_vrt.values[1, 0]} (literal sentinel survived)' # noqa: E501 assert via_vrt.attrs.get('nodata') == 65535.0 @@ -522,8 +522,8 @@ def test_vrt_uint16_no_nodata_keeps_dtype(tmp_path): tif = str(tmp_path / 'src_no_nodata_1564.tif') to_geotiff(da, tif, compression='none') vrt_path = str(tmp_path / 'src_no_nodata_1564.vrt') - write_vrt(vrt_path, [tif]) - via_vrt = read_vrt(vrt_path) + build_vrt(vrt_path, [tif]) + via_vrt = _read_vrt(vrt_path) assert via_vrt.dtype == np.uint16 np.testing.assert_array_equal(via_vrt.values, arr) @@ -536,8 +536,8 @@ def test_vrt_float_nodata_still_masks(tmp_path): tif = str(tmp_path / 'srcf_1564.tif') to_geotiff(da, tif, compression='none', nodata=-9999.0) vrt_path = str(tmp_path / 'srcf_1564.vrt') - write_vrt(vrt_path, [tif]) - via_vrt = read_vrt(vrt_path) + build_vrt(vrt_path, [tif]) + via_vrt = _read_vrt(vrt_path) assert via_vrt.dtype == np.float32 assert np.isnan(via_vrt.values[0, 2]) assert np.isnan(via_vrt.values[1, 1]) @@ -546,7 +546,7 @@ def test_vrt_float_nodata_still_masks(tmp_path): def _int_nodata_rewrite_vrt_nodata(vrt_path, new_nodata_text): """Rewrite the element of an existing VRT to a literal string so we can exercise fractional / out-of-range cases without - going through ``write_vrt`` (which only accepts numeric values).""" + going through ``build_vrt`` (which only accepts numeric values).""" with open(vrt_path, 'r') as f: xml = f.read() import re @@ -564,9 +564,9 @@ def test_vrt_fractional_nodata_is_not_masked(tmp_path): tif = str(tmp_path / 'frac_1564.tif') to_geotiff(da, tif, compression='none', nodata=1) vrt_path = str(tmp_path / 'frac_1564.vrt') - write_vrt(vrt_path, [tif]) + build_vrt(vrt_path, [tif]) _int_nodata_rewrite_vrt_nodata(vrt_path, '1.9') - via_vrt = read_vrt(vrt_path) + via_vrt = _read_vrt(vrt_path) assert via_vrt.dtype == np.uint16, f'Fractional NoDataValue must not trigger integer masking (got dtype {via_vrt.dtype}, pixel @[0,0]={via_vrt.values[0, 0]})' # noqa: E501 np.testing.assert_array_equal(via_vrt.values, arr) @@ -579,9 +579,9 @@ def test_vrt_out_of_range_nodata_is_not_masked(tmp_path): tif = str(tmp_path / 'oor_1564.tif') to_geotiff(da, tif, compression='none', nodata=0) vrt_path = str(tmp_path / 'oor_1564.vrt') - write_vrt(vrt_path, [tif]) + build_vrt(vrt_path, [tif]) _int_nodata_rewrite_vrt_nodata(vrt_path, '-1') - via_vrt = read_vrt(vrt_path) + via_vrt = _read_vrt(vrt_path) assert via_vrt.dtype == np.uint16, f'Out-of-range NoDataValue must not trigger integer masking (got dtype {via_vrt.dtype})' # noqa: E501 np.testing.assert_array_equal(via_vrt.values, arr) @@ -593,7 +593,7 @@ def test_vrt_open_geotiff_parity_uint16_nodata(tmp_path): _int_nodata_write_uint16_with_nodata_tif(tif, sentinel=65535) direct = open_geotiff(tif) vrt_path = str(tmp_path / 'parity_1564.vrt') - write_vrt(vrt_path, [tif]) + build_vrt(vrt_path, [tif]) via_vrt = open_geotiff(vrt_path) assert direct.dtype == via_vrt.dtype np.testing.assert_array_equal(np.isnan(direct.values), np.isnan(via_vrt.values), err_msg='VRT route should NaN-mask the same pixels as direct read') # noqa: E501 @@ -649,7 +649,7 @@ def test_default_mask_nodata_true_rewrites_float_sentinel(tmp_path): """ src, _ = _mask_nodata_float_write_float32_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', -9999.0) - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32 assert np.isnan(r.values[1, 1]) assert np.isnan(r.values[2, 1]) @@ -668,7 +668,7 @@ def test_eager_mask_nodata_false_preserves_float_sentinel(tmp_path): """ src, original = _mask_nodata_float_write_float32_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', -9999.0) - r = read_vrt(vrt, mask_nodata=False) + r = _read_vrt(vrt, mask_nodata=False) assert r.dtype == np.float32 assert not np.isnan(r.values).any() assert r.values[1, 1] == np.float32(-9999.0) @@ -688,7 +688,7 @@ def test_chunked_mask_nodata_false_preserves_float_sentinel(tmp_path): """ src, original = _mask_nodata_float_write_float32_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', -9999.0) - r = read_vrt(vrt, chunks=2, mask_nodata=False) + r = _read_vrt(vrt, chunks=2, mask_nodata=False) assert r.dtype == np.float32 computed = r.compute() assert not np.isnan(computed.values).any() @@ -709,8 +709,8 @@ def test_eager_and_chunked_agree_under_mask_nodata_false(tmp_path): """ src, _ = _mask_nodata_float_write_float32_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', -9999.0) - eager = read_vrt(vrt, mask_nodata=False) - chunked = read_vrt(vrt, chunks=2, mask_nodata=False).compute() + eager = _read_vrt(vrt, mask_nodata=False) + chunked = _read_vrt(vrt, chunks=2, mask_nodata=False).compute() np.testing.assert_array_equal(eager.values, chunked.values) assert eager.attrs.get('masked_nodata') == chunked.attrs.get('masked_nodata') @@ -724,7 +724,7 @@ def test_mask_nodata_false_float64_fractional_sentinel(tmp_path): """ src, original = _mask_nodata_float_write_float64_with_fractional_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float64', -9999.25, filename='float64_2158.vrt', shape=(2, 2)) # noqa: E501 - r = read_vrt(vrt, mask_nodata=False) + r = _read_vrt(vrt, mask_nodata=False) assert r.dtype == np.float64 assert r.values[1, 0] == -9999.25 np.testing.assert_array_equal(r.values, original) @@ -740,8 +740,8 @@ def test_masked_vs_unmasked_differ_only_at_sentinels(tmp_path): """ src, _ = _mask_nodata_float_write_float32_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', -9999.0) - masked = read_vrt(vrt).values - unmasked = read_vrt(vrt, mask_nodata=False).values + masked = _read_vrt(vrt).values + unmasked = _read_vrt(vrt, mask_nodata=False).values nan_positions = np.isnan(masked) sentinel_positions = unmasked == np.float32(-9999.0) np.testing.assert_array_equal(nan_positions, sentinel_positions) @@ -775,7 +775,7 @@ def test_int_source_float_vrt_mask_nodata_false_keeps_literal(tmp_path): """ src, _ = _mask_nodata_float_write_uint16_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', 65535, filename='int_float_2158.vrt', shape=(2, 2)) # noqa: E501 - r = read_vrt(vrt, mask_nodata=False) + r = _read_vrt(vrt, mask_nodata=False) assert r.dtype == np.float32 assert not np.isnan(r.values).any() assert r.values[1, 1] == np.float32(65535.0) @@ -793,7 +793,7 @@ def test_int_source_float_vrt_default_still_promotes(tmp_path): """ src, _ = _mask_nodata_float_write_uint16_with_sentinel(tmp_path) vrt = _mask_nodata_float_build_vrt(tmp_path, src, 'Float32', 65535, filename='int_float_default_2158.vrt', shape=(2, 2)) # noqa: E501 - r = read_vrt(vrt) + r = _read_vrt(vrt) assert r.dtype == np.float32 assert np.isnan(r.values[1, 1]) assert r.values[0, 0] == 1.0 @@ -985,7 +985,7 @@ def counting_parse(*args, **kwargs): counter['parses'] += 1 return real_parse(*args, **kwargs) monkeypatch.setattr(vrt_module, 'parse_vrt', counting_parse) - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) assert counter['parses'] == 1, f"expected 1 parse during construction, got {counter['parses']}" computed = result.compute() assert counter['parses'] == 1, f"expected 1 parse total (construction only); got {counter['parses']} -- per-chunk tasks are still reparsing" # noqa: E501 @@ -1009,7 +1009,7 @@ def counting_read_xml(*args, **kwargs): counter['reads'] += 1 return real_read_xml(*args, **kwargs) monkeypatch.setattr(vrt_module, '_read_vrt_xml', counting_read_xml) - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) assert counter['reads'] == 1, f"expected 1 XML file read during construction, got {counter['reads']}" # noqa: E501 result.compute() assert counter['reads'] == 1, f"expected 1 XML file read total; got {counter['reads']} -- per-chunk tasks are still re-opening the .vrt file" # noqa: E501 @@ -1046,8 +1046,8 @@ def test_chunked_matches_eager_after_refactor(single_parse_two_by_two_vrt_1825): regression in either call site would surface here. """ vrt_path, original = single_parse_two_by_two_vrt_1825 - eager = read_vrt(vrt_path) - chunked = read_vrt(vrt_path, chunks=(64, 64)).compute() + eager = _read_vrt(vrt_path) + chunked = _read_vrt(vrt_path, chunks=(64, 64)).compute() assert eager.dtype == chunked.dtype np.testing.assert_array_equal(eager.values, chunked.values) np.testing.assert_array_equal(eager.values, original) @@ -1070,7 +1070,7 @@ def counting_parse(*args, **kwargs): parse_calls['n'] += 1 return real_parse(*args, **kwargs) monkeypatch.setattr(vrt_module, 'parse_vrt', counting_parse) - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) parses_after_construction = parse_calls['n'] da_arr = result.data if isinstance(da_arr, da.Array): @@ -1080,10 +1080,10 @@ def counting_parse(*args, **kwargs): def test_parsed_kwarg_does_not_mutate_caller_holes(single_parse_single_tile_vrt_1825): - """``read_vrt(parsed=...)`` must not mutate the caller's ``holes``. + """``_read_vrt(parsed=...)`` must not mutate the caller's ``holes``. The chunked dispatcher threads a single parsed ``VRTDataset`` into - every per-chunk task. ``read_vrt`` appends skipped-source records to + every per-chunk task. ``_read_vrt`` appends skipped-source records to ``vrt.holes`` when a backing file is missing; without a defensive copy the appends would land on the dispatcher's shared object and leak across tasks (racy under the threaded scheduler, and @@ -1110,7 +1110,7 @@ def test_parsed_kwarg_does_not_mutate_caller_holes(single_parse_single_tile_vrt_ # --------------------------------------------------------------------------- -# write_vrt escapes XML special chars +# build_vrt escapes XML special chars # --------------------------------------------------------------------------- @@ -1127,7 +1127,7 @@ def xml_escape_sample_tif(tmp_path): def test_crs_wkt_with_xml_special_chars_round_trips(xml_escape_sample_tif, tmp_path): - """A WKT containing ``& < > " '`` must round-trip through write_vrt / + """A WKT containing ``& < > " '`` must round-trip through build_vrt / parse_vrt unchanged (the entities are escaped on the way out and decoded on the way in).""" nasty_wkt = 'GEOGCS["spec & with "quotes" and \'apostrophes\'"]' @@ -1174,7 +1174,7 @@ def test_source_filename_with_ampersand_round_trips(tmp_path): def test_written_vrt_is_well_formed_xml(xml_escape_sample_tif, tmp_path): - """Sanity check: the bytes written by write_vrt always parse cleanly + """Sanity check: the bytes written by build_vrt always parse cleanly as XML, even when crs_wkt carries every XML predefined entity.""" nasty = '< & > " \'' vrt_path = str(tmp_path / 'wf.vrt') @@ -1185,7 +1185,7 @@ def test_written_vrt_is_well_formed_xml(xml_escape_sample_tif, tmp_path): # --------------------------------------------------------------------------- -# XML size cap on eager read_vrt +# XML size cap on eager _read_vrt # --------------------------------------------------------------------------- @@ -1251,7 +1251,7 @@ def test_invalid_cap_raises_value_error(tmp_path, monkeypatch, bad_value): # --------------------------------------------------------------------------- -# XML size cap on chunked read_vrt +# XML size cap on chunked _read_vrt # --------------------------------------------------------------------------- @@ -1274,13 +1274,13 @@ def _xml_size_cap_chunked_write_vrt(td: str, *, pad_bytes: int = 0) -> str: def test_chunked_read_vrt_honors_xml_cap(tmp_path, monkeypatch): - """``read_vrt(chunks=...)`` rejects oversized VRT XML.""" + """``_read_vrt(chunks=...)`` rejects oversized VRT XML.""" td = str(tmp_path) _xml_size_cap_chunked_write_source(td) monkeypatch.setenv('XRSPATIAL_VRT_MAX_XML_BYTES', '1024') vrt_path = _xml_size_cap_chunked_write_vrt(td, pad_bytes=4096) with pytest.raises(ValueError) as exc_info: - read_vrt(vrt_path, chunks=10) + _read_vrt(vrt_path, chunks=10) msg = str(exc_info.value) assert 'XRSPATIAL_VRT_MAX_XML_BYTES' in msg assert '1,024' in msg @@ -1291,7 +1291,7 @@ def test_chunked_read_vrt_under_default_cap(tmp_path): td = str(tmp_path) _xml_size_cap_chunked_write_source(td) vrt_path = _xml_size_cap_chunked_write_vrt(td) - arr = read_vrt(vrt_path, chunks=10) + arr = _read_vrt(vrt_path, chunks=10) assert arr.shape == (10, 10) assert arr.dtype == np.uint8 @@ -1302,7 +1302,7 @@ def test_chunked_read_vrt_raised_cap_allows_padded(tmp_path, monkeypatch): _xml_size_cap_chunked_write_source(td) vrt_path = _xml_size_cap_chunked_write_vrt(td, pad_bytes=4096) monkeypatch.setenv('XRSPATIAL_VRT_MAX_XML_BYTES', str(1024 * 1024)) - arr = read_vrt(vrt_path, chunks=10) + arr = _read_vrt(vrt_path, chunks=10) assert arr.shape == (10, 10) @@ -1400,14 +1400,14 @@ def _metadata_parity_read_dask_chunks_2(vrt_path: str): def _metadata_parity_read_gpu_eager(vrt_path: str): - """GPU eager via ``read_vrt(gpu=True)``. + """GPU eager via ``_read_vrt(gpu=True)``. ``open_geotiff(..., gpu=True)`` rejects ``.vrt`` sources up front - (the dispatcher routes ``.vrt`` to ``read_vrt`` and ``read_vrt`` + (the dispatcher routes ``.vrt`` to ``_read_vrt`` and ``_read_vrt`` owns the ``gpu`` kwarg, see ``_backends/vrt.py``). Use the direct entry point here so the GPU eager path is exercised. """ - return read_vrt(vrt_path, gpu=True) + return _read_vrt(vrt_path, gpu=True) _BACKENDS = [pytest.param('numpy', _metadata_parity_read_eager_numpy, id='numpy'), pytest.param('dask', _metadata_parity_read_dask, id='dask'), pytest.param('gpu', _metadata_parity_read_gpu_eager, id='gpu', marks=requires_gpu)] # noqa: E501 @@ -1594,7 +1594,7 @@ def test_mixed_crs_vrt_does_not_silently_flatten(tmp_path): """ vrt = _metadata_parity_write_mixed_crs_vrt(tmp_path) with pytest.raises(VRTUnsupportedError): - read_vrt(vrt) + _read_vrt(vrt) def _metadata_parity_write_mixed_nodata_vrt(tmp_path: pathlib.Path) -> str: @@ -1643,7 +1643,7 @@ def test_mixed_nodata_vrt_opt_in_first_succeeds(tmp_path): is the legacy behaviour callers may explicitly want. """ vrt = _metadata_parity_write_mixed_nodata_vrt(tmp_path) - result = read_vrt(vrt, band_nodata='first') + result = _read_vrt(vrt, band_nodata='first') assert result.shape == (2, 2, 2) @@ -1677,7 +1677,7 @@ def test_unsupported_resample_alg_raises(tmp_path): """ vrt = _metadata_parity_write_unsupported_resample_vrt(tmp_path) with pytest.raises((NotImplementedError, VRTUnsupportedError), match='Bilinear'): - read_vrt(vrt) + _read_vrt(vrt) def _metadata_parity_write_bad_srcrect_vrt(tmp_path: pathlib.Path, *, x_size: int = -50) -> str: @@ -1702,7 +1702,7 @@ def test_negative_srcrect_size_rejected(tmp_path): """ vrt = _metadata_parity_write_bad_srcrect_vrt(tmp_path, x_size=-50) with pytest.raises((ValueError, VRTUnsupportedError), match='SrcRect.*negative'): - read_vrt(vrt) + _read_vrt(vrt) def _metadata_parity_write_bad_dstrect_vrt(tmp_path: pathlib.Path, *, x_size: int = -10) -> str: @@ -1734,7 +1734,7 @@ def test_negative_dstrect_size_rejected(tmp_path): """ vrt = _metadata_parity_write_bad_dstrect_vrt(tmp_path, x_size=-10) with pytest.raises((ValueError, VRTUnsupportedError), match='DstRect.*negative'): - read_vrt(vrt) + _read_vrt(vrt) def _metadata_parity_write_missing_source_vrt(tmp_path: pathlib.Path, *, name: str = 'tmp_2321_missing.vrt') -> str: # noqa: E501 @@ -1757,7 +1757,7 @@ def test_missing_sources_raise_eager(tmp_path): must abort the read up front on the eager path.""" vrt = _metadata_parity_write_missing_source_vrt(tmp_path, name='tmp_2321_miss_eager.vrt') with pytest.raises((OSError, ValueError, FileNotFoundError)): - read_vrt(vrt) + _read_vrt(vrt) def test_missing_sources_raise_dask(tmp_path): @@ -1779,7 +1779,7 @@ def test_missing_sources_warn_records_holes(tmp_path): The lenient path must emit ``GeoTIFFFallbackWarning`` and populate ``attrs['vrt_holes']`` so callers branching on the attr can detect a partial mosaic. This is the documented contract; - the test pins it via the public ``read_vrt`` entry point so a + the test pins it via the public ``_read_vrt`` entry point so a regression in the warn-policy attr emission surfaces. The public API exposes ``'warn'`` as the lenient option (``'skip'`` @@ -1788,7 +1788,7 @@ def test_missing_sources_warn_records_holes(tmp_path): """ vrt = _metadata_parity_write_missing_source_vrt(tmp_path, name='tmp_2321_miss_warn.vrt') with pytest.warns(GeoTIFFFallbackWarning, match='could not be read'): - result = read_vrt(vrt, missing_sources='warn') + result = _read_vrt(vrt, missing_sources='warn') assert 'vrt_holes' in result.attrs, "missing_sources='warn' did not stamp attrs['vrt_holes']" holes = result.attrs['vrt_holes'] assert len(holes) == 1 diff --git a/xrspatial/geotiff/tests/vrt/test_missing_sources.py b/xrspatial/geotiff/tests/vrt/test_missing_sources.py index e20d3c78c..09e14ec22 100644 --- a/xrspatial/geotiff/tests/vrt/test_missing_sources.py +++ b/xrspatial/geotiff/tests/vrt/test_missing_sources.py @@ -18,7 +18,7 @@ * Internal ``_vrt.read_vrt`` entry point default-raise + explicit-warn + ``XRSPATIAL_GEOTIFF_STRICT=1`` override. -* Public ``read_vrt`` / ``open_geotiff('.vrt')`` default-raise + +* Public ``_read_vrt`` / ``open_geotiff('.vrt')`` default-raise + explicit-warn. * Chunked-path missing-source policy: ``vrt_holes`` at build, raise-at-build, per-task compute warnings, window / band scoping, @@ -33,7 +33,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, read_vrt, to_geotiff +from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, _read_vrt, to_geotiff from xrspatial.geotiff._vrt import read_vrt as _internal_read_vrt PRESENT_FILL = 7.0 @@ -118,11 +118,11 @@ def _write_partial_float_vrt(tmp_path) -> tuple[str, str, str]: # --------------------------------------------------------------------------- def _eager_reader(source, **kwargs): - return read_vrt(source, **kwargs) + return _read_vrt(source, **kwargs) def _dask_reader(source, **kwargs): - # ``open_geotiff`` routes ``.vrt`` to ``read_vrt`` and forwards + # ``open_geotiff`` routes ``.vrt`` to ``_read_vrt`` and forwards # ``chunks=`` / ``missing_sources=`` unchanged. Using a small chunk # size keeps the partial mosaic split across multiple tasks so the # lazy path is genuinely exercised. @@ -164,7 +164,7 @@ def test_eager_byte_default_raises(self, tmp_path): disk.""" vrt = _write_byte_missing_vrt(tmp_path) with pytest.raises((OSError, ValueError)): - read_vrt(vrt) + _read_vrt(vrt) # --------------------------------------------------------------------------- @@ -186,7 +186,7 @@ def test_explicit_raise_matches_default(self, reader, tmp_path): def test_eager_byte_explicit_raise(self, tmp_path): vrt = _write_byte_missing_vrt(tmp_path) with pytest.raises((OSError, ValueError)): - read_vrt(vrt, missing_sources="raise") + _read_vrt(vrt, missing_sources="raise") # --------------------------------------------------------------------------- @@ -213,7 +213,7 @@ def test_eager_warn_emits_and_fills(self, tmp_path): with pytest.warns( GeoTIFFFallbackWarning, match="missing_source.tif", ): - da = read_vrt(vrt_path, missing_sources="warn") + da = _read_vrt(vrt_path, missing_sources="warn") assert "vrt_holes" in da.attrs sources = [h["source"] for h in da.attrs["vrt_holes"]] @@ -264,7 +264,7 @@ def test_eager_byte_warn_records_hole(self, tmp_path): populated even when there is no present half.""" vrt = _write_byte_missing_vrt(tmp_path) with pytest.warns(GeoTIFFFallbackWarning, match="could not be read"): - da = read_vrt(vrt, missing_sources="warn") + da = _read_vrt(vrt, missing_sources="warn") assert "vrt_holes" in da.attrs assert da.attrs["vrt_holes"][0]["source"].endswith("missing.tif") @@ -312,14 +312,14 @@ def test_eager_byte_invalid_policy(self, tmp_path): path stays exercised.""" vrt = _write_byte_missing_vrt(tmp_path) with pytest.raises(ValueError, match="missing_sources"): - read_vrt(vrt, missing_sources="ignore") + _read_vrt(vrt, missing_sources="ignore") # =========================================================================== # Internal ``_vrt.read_vrt`` entry point (was # test_vrt_missing_sources_default_raise_1843.py). # -# The public matrix above exercises the package-level ``read_vrt`` / +# The public matrix above exercises the package-level ``_read_vrt`` / # ``open_geotiff`` surface. These cases pin the internal # ``xrspatial.geotiff._vrt.read_vrt`` entry point directly, including the # ``XRSPATIAL_GEOTIFF_STRICT=1`` module-wide override that wins over a @@ -389,7 +389,7 @@ def test_internal_strict_env_still_raises_under_warn( # =========================================================================== -# Public default ``missing_sources='raise'`` on read_vrt + open_geotiff +# Public default ``missing_sources='raise'`` on _read_vrt + open_geotiff # # Pins that the public wrapper's default matches the internal # ``_vrt.read_vrt`` default rather than silently overriding it with the @@ -414,10 +414,10 @@ def _write_public_missing_source_vrt(path): class TestPublicDefaultMissingSources: - """Public ``read_vrt`` / ``open_geotiff('.vrt')`` default to ``'raise'``.""" + """Public ``_read_vrt`` / ``open_geotiff('.vrt')`` default to ``'raise'``.""" def test_public_read_vrt_default_raises(self, tmp_path): - """Public ``read_vrt`` with no ``missing_sources`` kwarg must raise. + """Public ``_read_vrt`` with no ``missing_sources`` kwarg must raise. The default is aligned to the internal ``_vrt.read_vrt`` default of ``'raise'`` so the unreadable source halts the call instead of @@ -426,15 +426,15 @@ def test_public_read_vrt_default_raises(self, tmp_path): vrt = tmp_path / "tmp_1860_public_default_raise.vrt" _write_public_missing_source_vrt(vrt) with pytest.raises((OSError, ValueError)): - read_vrt(str(vrt)) + _read_vrt(str(vrt)) def test_open_geotiff_vrt_default_raises(self, tmp_path): """``open_geotiff(vrt_path)`` with no ``missing_sources`` kwarg must raise on an unreadable backing source. - ``open_geotiff`` forwards ``missing_sources`` to ``read_vrt`` only + ``open_geotiff`` forwards ``missing_sources`` to ``_read_vrt`` only when the caller passed it explicitly; otherwise the public - ``read_vrt`` default applies. + ``_read_vrt`` default applies. """ vrt = tmp_path / "tmp_1860_open_geotiff_default_raise.vrt" _write_public_missing_source_vrt(vrt) @@ -445,11 +445,11 @@ def test_public_read_vrt_explicit_warn_preserves_lenient_behaviour( self, tmp_path, ): """``missing_sources='warn'`` is still the escape hatch for partial - mosaics on the public ``read_vrt`` API.""" + mosaics on the public ``_read_vrt`` API.""" vrt = tmp_path / "tmp_1860_public_explicit_warn.vrt" _write_public_missing_source_vrt(vrt) with pytest.warns(GeoTIFFFallbackWarning, match="could not be read"): - da = read_vrt(str(vrt), missing_sources='warn') + da = _read_vrt(str(vrt), missing_sources='warn') assert 'vrt_holes' in da.attrs assert da.attrs['vrt_holes'][0]['source'].endswith('missing_1860.tif') @@ -515,11 +515,11 @@ def _chunked_make_partial_vrt(tmp_path) -> tuple[str, str]: class TestChunkedMissingSourcesWarn: - """``read_vrt(chunks=N, missing_sources='warn')`` records holes at build.""" + """``_read_vrt(chunks=N, missing_sources='warn')`` records holes at build.""" def test_vrt_holes_populated_at_build(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) - result = read_vrt(vrt_path, chunks=4, missing_sources="warn") + result = _read_vrt(vrt_path, chunks=4, missing_sources="warn") assert "vrt_holes" in result.attrs, ( "Chunked path must populate vrt_holes at build time so " "callers can detect partial mosaics without forcing a compute." @@ -535,7 +535,7 @@ def test_compute_emits_per_task_warning(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") - result = read_vrt(vrt_path, chunks=4, missing_sources="warn") + result = _read_vrt(vrt_path, chunks=4, missing_sources="warn") computed = result.compute() messages = [str(w.message) for w in caught if isinstance(w.message, GeoTIFFFallbackWarning)] @@ -551,13 +551,13 @@ def test_compute_emits_per_task_warning(self, tmp_path): def test_chunks_tuple_form(self, tmp_path): """Tuple ``chunks=(h, w)`` threads through identically.""" vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) - result = read_vrt(vrt_path, chunks=(2, 4), missing_sources="warn") + result = _read_vrt(vrt_path, chunks=(2, 4), missing_sources="warn") assert "vrt_holes" in result.attrs assert len(result.attrs["vrt_holes"]) == 1 class TestChunkedMissingSourcesRaiseSmoke: - """``read_vrt(chunks=N, missing_sources='raise')`` fails at build. + """``_read_vrt(chunks=N, missing_sources='raise')`` fails at build. The detailed raise-at-build matrix (window / band scoping, multi-source preview, strict env) lives in the 2265 section below; this keeps the @@ -567,17 +567,17 @@ class TestChunkedMissingSourcesRaiseSmoke: def test_build_raises_immediately(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError, match="missing.tif"): - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") def test_build_raise_message_mentions_policy_kwarg(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError) as excinfo: - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") assert "missing_sources='warn'" in str(excinfo.value) def test_window_past_missing_succeeds_under_raise(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) - result = read_vrt( + result = _read_vrt( vrt_path, chunks=4, window=(0, 0, 4, 4), missing_sources="raise", ) @@ -592,7 +592,7 @@ def test_band_selection_single_band_still_raises(self, tmp_path): multiband cases in the 2265 section below.""" vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt(vrt_path, chunks=4, band=0, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, band=0, missing_sources="raise") class TestChunkedMissingSourcesDefault: @@ -601,7 +601,7 @@ class TestChunkedMissingSourcesDefault: def test_chunked_default_raises_at_build(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError, match="missing.tif"): - read_vrt(vrt_path, chunks=4) + _read_vrt(vrt_path, chunks=4) class TestChunkedMissingSourcesValidation: @@ -610,14 +610,14 @@ class TestChunkedMissingSourcesValidation: def test_invalid_policy_raises_at_build(self, tmp_path): vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(ValueError, match="missing_sources"): - read_vrt(vrt_path, chunks=4, missing_sources="ignore") + _read_vrt(vrt_path, chunks=4, missing_sources="ignore") def test_invalid_policy_raises_without_chunks_too(self, tmp_path): """The eager path also rejects the bad value; callers see the same error whether or not they pass ``chunks=``.""" vrt_path, _ = _chunked_make_partial_vrt(str(tmp_path)) with pytest.raises(ValueError, match="missing_sources"): - read_vrt(vrt_path, missing_sources="ignore") + _read_vrt(vrt_path, missing_sources="ignore") # =========================================================================== @@ -741,21 +741,21 @@ class TestRaiseAtBuild: def test_build_raises_immediately(self, tmp_path): vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError, match="missing_2265_h"): - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") def test_default_raises_at_build(self, tmp_path): """The public default is ``'raise'`` so dropping the kwarg hits the same fast-fail path.""" vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt(vrt_path, chunks=4) + _read_vrt(vrt_path, chunks=4) def test_error_message_mentions_opt_in(self, tmp_path): """The exception text tells the caller how to opt into the lenient path.""" vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError) as excinfo: - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") msg = str(excinfo.value) assert "missing_sources='warn'" in msg assert "partial mosaic" in msg @@ -766,7 +766,7 @@ class TestRaiseAtBuildWindowScoping: def test_window_past_missing_does_not_raise(self, tmp_path): vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) - result = read_vrt( + result = _read_vrt( vrt_path, chunks=4, window=(0, 0, 4, 4), missing_sources="raise", ) @@ -778,7 +778,7 @@ def test_window_past_missing_does_not_raise(self, tmp_path): def test_window_intersecting_missing_raises(self, tmp_path): vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt( + _read_vrt( vrt_path, chunks=4, window=(0, 4, 4, 8), missing_sources="raise", ) @@ -791,7 +791,7 @@ def test_band_select_skips_other_bands_missing_source(self, tmp_path): """``band=1`` reads band 2 only; band 1's missing source is irrelevant to the graph, so the build must not raise.""" vrt_path = _raise_make_multiband_partial_vrt(str(tmp_path)) - result = read_vrt( + result = _read_vrt( vrt_path, chunks=4, band=1, missing_sources="raise", ) computed = result.compute() @@ -802,12 +802,12 @@ def test_band_select_skips_other_bands_missing_source(self, tmp_path): def test_band_select_on_missing_band_raises(self, tmp_path): vrt_path = _raise_make_multiband_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt(vrt_path, chunks=4, band=0, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, band=0, missing_sources="raise") def test_no_band_restriction_raises(self, tmp_path): vrt_path = _raise_make_multiband_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") class TestRaiseAtBuildWarnPreserved: @@ -815,7 +815,7 @@ class TestRaiseAtBuildWarnPreserved: def test_warn_records_holes_at_build(self, tmp_path): vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) - result = read_vrt(vrt_path, chunks=4, missing_sources="warn") + result = _read_vrt(vrt_path, chunks=4, missing_sources="warn") assert "vrt_holes" in result.attrs assert len(result.attrs["vrt_holes"]) == 1 assert result.attrs["vrt_holes"][0]["source"].endswith( @@ -826,7 +826,7 @@ def test_warn_compute_emits_per_task_warning(self, tmp_path): vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") - result = read_vrt(vrt_path, chunks=4, missing_sources="warn") + result = _read_vrt(vrt_path, chunks=4, missing_sources="warn") computed = result.compute() messages = [str(w.message) for w in caught if isinstance(w.message, GeoTIFFFallbackWarning)] @@ -845,7 +845,7 @@ def test_two_missing_sources_listed_with_count(self, tmp_path): """All missing sources fit in the preview (n=2 <= preview cap).""" vrt_path = _raise_make_multi_missing_vrt(str(tmp_path), n_missing=2) with pytest.raises(FileNotFoundError) as excinfo: - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") msg = str(excinfo.value) assert "missing_2265_multi_0" in msg assert "missing_2265_multi_1" in msg @@ -857,7 +857,7 @@ def test_many_missing_sources_truncated_with_more_suffix(self, tmp_path): n = 5 vrt_path = _raise_make_multi_missing_vrt(str(tmp_path), n_missing=n) with pytest.raises(FileNotFoundError) as excinfo: - read_vrt(vrt_path, chunks=4, missing_sources="raise") + _read_vrt(vrt_path, chunks=4, missing_sources="raise") msg = str(excinfo.value) assert "missing_2265_multi_0" in msg assert f"missing_2265_multi_{n - 1}" not in msg @@ -872,11 +872,11 @@ def test_strict_overrides_warn_kwarg(self, tmp_path, monkeypatch): monkeypatch.setenv("XRSPATIAL_GEOTIFF_STRICT", "1") vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) with pytest.raises(FileNotFoundError): - read_vrt(vrt_path, chunks=4, missing_sources="warn") + _read_vrt(vrt_path, chunks=4, missing_sources="warn") def test_strict_off_warn_still_warns(self, tmp_path, monkeypatch): """Without strict mode, ``'warn'`` keeps warning.""" monkeypatch.delenv("XRSPATIAL_GEOTIFF_STRICT", raising=False) vrt_path = _raise_make_horizontal_partial_vrt(str(tmp_path)) - result = read_vrt(vrt_path, chunks=4, missing_sources="warn") + result = _read_vrt(vrt_path, chunks=4, missing_sources="warn") assert "vrt_holes" in result.attrs diff --git a/xrspatial/geotiff/tests/vrt/test_parity.py b/xrspatial/geotiff/tests/vrt/test_parity.py index a210bc6d5..adcc0b316 100644 --- a/xrspatial/geotiff/tests/vrt/test_parity.py +++ b/xrspatial/geotiff/tests/vrt/test_parity.py @@ -7,11 +7,11 @@ ``georef_status``) parity, sidecar-vs-inline-overview attrs, and the windowed coord / transform shift. * Cross-backend parity for the VRT finalization pipeline: VRT eager vs - ``open_geotiff`` and VRT chunked vs ``read_geotiff_dask`` for the five + ``open_geotiff`` and VRT chunked vs ``_read_geotiff_dask`` for the five canonical georef states, ``band_nodata='first'`` per-band attrs, ``dtype=`` no-sentinel branch, ``missing_sources='warn'`` vrt_holes, and eager/chunked internal parity. -* Backend / parameter coverage for ``read_vrt``: the GPU and dask+GPU +* Backend / parameter coverage for ``_read_vrt``: the GPU and dask+GPU decode paths, ``dtype=`` / ``name=`` kwargs, and the file-like + backend-kwarg rejection on ``open_geotiff``. @@ -36,7 +36,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import open_geotiff, read_geotiff_dask, read_vrt, to_geotiff +from xrspatial.geotiff import open_geotiff, _read_geotiff_dask, _read_vrt, to_geotiff from xrspatial.geotiff._attrs import (GEOREF_STATUS_CRS_ONLY, GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_ROTATED_DROPPED, GEOREF_STATUS_TRANSFORM_ONLY) @@ -646,7 +646,7 @@ def test_vrt_eager_full_matches_open_geotiff(tmp_path): canonical helper-stamped attrs as the underlying TIFF read.""" tiff, vrt = _make_full_pair(tmp_path, 'full_2180') tiff_attrs = _shared_canonical_attrs(dict(open_geotiff(tiff).attrs)) - vrt_attrs = _shared_canonical_attrs(dict(read_vrt(vrt).attrs)) + vrt_attrs = _shared_canonical_attrs(dict(_read_vrt(vrt).attrs)) assert tiff_attrs == vrt_attrs, ( f"TIFF/VRT attrs diverged:\n" f" tiff only: {set(tiff_attrs) - set(vrt_attrs)}\n" @@ -655,7 +655,7 @@ def test_vrt_eager_full_matches_open_geotiff(tmp_path): f"{[k for k in set(tiff_attrs) & set(vrt_attrs) if tiff_attrs[k] != vrt_attrs[k]]}" ) full_tiff_attrs = dict(open_geotiff(tiff).attrs) - full_vrt_attrs = dict(read_vrt(vrt).attrs) + full_vrt_attrs = dict(_read_vrt(vrt).attrs) assert full_tiff_attrs['crs'] == full_vrt_attrs['crs'] == 4326 assert len(full_tiff_attrs['transform']) == 6 assert len(full_vrt_attrs['transform']) == 6 @@ -664,7 +664,7 @@ def test_vrt_eager_full_matches_open_geotiff(tmp_path): def test_vrt_eager_transform_only_matches_open_geotiff(tmp_path): tiff, vrt = _make_transform_only_pair(tmp_path, 'tonly_2180') tiff_attrs = _shared_canonical_attrs(dict(open_geotiff(tiff).attrs)) - vrt_attrs = _shared_canonical_attrs(dict(read_vrt(vrt).attrs)) + vrt_attrs = _shared_canonical_attrs(dict(_read_vrt(vrt).attrs)) assert tiff_attrs == vrt_attrs assert tiff_attrs['georef_status'] == GEOREF_STATUS_TRANSFORM_ONLY @@ -672,7 +672,7 @@ def test_vrt_eager_transform_only_matches_open_geotiff(tmp_path): def test_vrt_eager_crs_only_matches_open_geotiff(tmp_path): tiff, vrt = _make_crs_only_pair(tmp_path, 'crsonly_2180') tiff_attrs = _shared_canonical_attrs(dict(open_geotiff(tiff).attrs)) - vrt_attrs = _shared_canonical_attrs(dict(read_vrt(vrt).attrs)) + vrt_attrs = _shared_canonical_attrs(dict(_read_vrt(vrt).attrs)) assert tiff_attrs == vrt_attrs assert tiff_attrs['georef_status'] == GEOREF_STATUS_CRS_ONLY @@ -680,7 +680,7 @@ def test_vrt_eager_crs_only_matches_open_geotiff(tmp_path): def test_vrt_eager_none_matches_open_geotiff(tmp_path): tiff, vrt = _make_none_pair(tmp_path, 'none_2180') tiff_attrs = _shared_canonical_attrs(dict(open_geotiff(tiff).attrs)) - vrt_attrs = _shared_canonical_attrs(dict(read_vrt(vrt).attrs)) + vrt_attrs = _shared_canonical_attrs(dict(_read_vrt(vrt).attrs)) assert tiff_attrs == vrt_attrs assert tiff_attrs['georef_status'] == GEOREF_STATUS_NONE @@ -690,7 +690,7 @@ def test_vrt_eager_rotated_dropped_matches_open_geotiff(tmp_path): in ``rotated_dropped`` and the helper drops crs / transform / crs_wkt while emitting ``rotated_affine`` plus the no-georef marker.""" _, vrt = _make_rotated_pair(tmp_path, 'rot_2180') - attrs = dict(read_vrt(vrt, allow_rotated=True).attrs) + attrs = dict(_read_vrt(vrt, allow_rotated=True).attrs) assert attrs['georef_status'] == GEOREF_STATUS_ROTATED_DROPPED assert attrs.get(_NO_GEOREF_KEY) is True assert 'rotated_affine' in attrs @@ -702,10 +702,10 @@ def test_vrt_eager_rotated_dropped_matches_open_geotiff(tmp_path): def test_vrt_chunked_full_matches_dask(tmp_path): tiff, vrt = _make_full_pair(tmp_path, 'full_chunked_2180') tiff_attrs = _shared_canonical_attrs( - dict(read_geotiff_dask(tiff, chunks=2).attrs) + dict(_read_geotiff_dask(tiff, chunks=2).attrs) ) vrt_attrs = _shared_canonical_attrs( - dict(read_vrt(vrt, chunks=2).attrs) + dict(_read_vrt(vrt, chunks=2).attrs) ) assert tiff_attrs == vrt_attrs @@ -713,10 +713,10 @@ def test_vrt_chunked_full_matches_dask(tmp_path): def test_vrt_chunked_transform_only_matches_dask(tmp_path): tiff, vrt = _make_transform_only_pair(tmp_path, 'tonly_chunked_2180') tiff_attrs = _shared_canonical_attrs( - dict(read_geotiff_dask(tiff, chunks=2).attrs) + dict(_read_geotiff_dask(tiff, chunks=2).attrs) ) vrt_attrs = _shared_canonical_attrs( - dict(read_vrt(vrt, chunks=2).attrs) + dict(_read_vrt(vrt, chunks=2).attrs) ) assert tiff_attrs == vrt_attrs @@ -724,10 +724,10 @@ def test_vrt_chunked_transform_only_matches_dask(tmp_path): def test_vrt_chunked_crs_only_matches_dask(tmp_path): tiff, vrt = _make_crs_only_pair(tmp_path, 'crsonly_chunked_2180') tiff_attrs = _shared_canonical_attrs( - dict(read_geotiff_dask(tiff, chunks=2).attrs) + dict(_read_geotiff_dask(tiff, chunks=2).attrs) ) vrt_attrs = _shared_canonical_attrs( - dict(read_vrt(vrt, chunks=2).attrs) + dict(_read_vrt(vrt, chunks=2).attrs) ) assert tiff_attrs == vrt_attrs @@ -735,10 +735,10 @@ def test_vrt_chunked_crs_only_matches_dask(tmp_path): def test_vrt_chunked_none_matches_dask(tmp_path): tiff, vrt = _make_none_pair(tmp_path, 'none_chunked_2180') tiff_attrs = _shared_canonical_attrs( - dict(read_geotiff_dask(tiff, chunks=2).attrs) + dict(_read_geotiff_dask(tiff, chunks=2).attrs) ) vrt_attrs = _shared_canonical_attrs( - dict(read_vrt(vrt, chunks=2).attrs) + dict(_read_vrt(vrt, chunks=2).attrs) ) assert tiff_attrs == vrt_attrs @@ -749,7 +749,7 @@ def test_vrt_eager_none_synthesizes_pixel_coords(tmp_path): no-georef read instead of dropping coords entirely.""" tiff, vrt = _make_none_pair(tmp_path, 'none_coords_2818') ref = open_geotiff(tiff) - vrt_da = read_vrt(vrt) + vrt_da = _read_vrt(vrt) assert ref.attrs['georef_status'] == GEOREF_STATUS_NONE assert vrt_da.attrs['georef_status'] == GEOREF_STATUS_NONE assert 'x' in vrt_da.coords and 'y' in vrt_da.coords @@ -766,8 +766,8 @@ def test_vrt_chunked_none_synthesizes_pixel_coords(tmp_path): synthesise the same integer x/y pixel coords as the non-VRT dask no-georef read rather than dropping coords entirely.""" tiff, vrt = _make_none_pair(tmp_path, 'none_coords_chunked_2818') - ref = read_geotiff_dask(tiff, chunks=2) - vrt_da = read_vrt(vrt, chunks=2) + ref = _read_geotiff_dask(tiff, chunks=2) + vrt_da = _read_vrt(vrt, chunks=2) assert ref.attrs['georef_status'] == GEOREF_STATUS_NONE assert vrt_da.attrs['georef_status'] == GEOREF_STATUS_NONE assert 'x' in vrt_da.coords and 'y' in vrt_da.coords @@ -786,8 +786,8 @@ def test_vrt_none_windowed_synthesizes_offset_pixel_coords(tmp_path): tiff, vrt = _make_none_pair(tmp_path, 'none_coords_win_2818') window = (1, 2, 4, 4) ref = open_geotiff(tiff, window=window) - eager = read_vrt(vrt, window=window) - chunked = read_vrt(vrt, window=window, chunks=2) + eager = _read_vrt(vrt, window=window) + chunked = _read_vrt(vrt, window=window, chunks=2) for label, actual in (('eager', eager), ('chunked', chunked)): assert 'x' in actual.coords and 'y' in actual.coords, label np.testing.assert_array_equal( @@ -800,7 +800,7 @@ def test_vrt_none_windowed_synthesizes_offset_pixel_coords(tmp_path): def test_vrt_chunked_rotated_dropped(tmp_path): _, vrt = _make_rotated_pair(tmp_path, 'rot_chunked_2180') - attrs = dict(read_vrt(vrt, allow_rotated=True, chunks=2).attrs) + attrs = dict(_read_vrt(vrt, allow_rotated=True, chunks=2).attrs) assert attrs['georef_status'] == GEOREF_STATUS_ROTATED_DROPPED assert attrs.get(_NO_GEOREF_KEY) is True assert 'rotated_affine' in attrs @@ -845,7 +845,7 @@ def test_band_nodata_first_band_attrs(tmp_path): """``band=1`` with ``band_nodata='first'`` surfaces band 1's sentinel on attrs and masks against it.""" vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band=1, band_nodata='first') + r = _read_vrt(vrt_path, band=1, band_nodata='first') assert r.attrs['nodata'] == 65000.0 assert r.attrs['masked_nodata'] is True assert np.isnan(r.values[1, 1]) @@ -855,7 +855,7 @@ def test_band_nodata_first_band_attrs(tmp_path): def test_band_nodata_chunked_first_band_attrs(tmp_path): """The chunked path threads the same per-band sentinel onto attrs.""" vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path) - r = read_vrt(vrt_path, band=1, band_nodata='first', chunks=2) + r = _read_vrt(vrt_path, band=1, band_nodata='first', chunks=2) assert r.attrs['nodata'] == 65000.0 assert r.attrs['masked_nodata'] is True assert 'nodata_pixels_present' not in r.attrs @@ -879,7 +879,7 @@ def test_dtype_cast_no_sentinel_omits_attr_eager(tmp_path): """Eager VRT with ``dtype=`` and no declared sentinel: ``nodata_dtype_cast`` stays absent.""" vrt = _make_no_sentinel_vrt(tmp_path, 'no_sentinel_eager_2180') - r = read_vrt(vrt, dtype=np.float64) + r = _read_vrt(vrt, dtype=np.float64) assert r.dtype == np.float64 assert 'nodata' not in r.attrs assert 'masked_nodata' not in r.attrs @@ -890,7 +890,7 @@ def test_dtype_cast_no_sentinel_omits_attr_chunked(tmp_path): """Chunked VRT with ``dtype=`` and no declared sentinel: same ``nodata_dtype_cast`` pop as the eager branch.""" vrt = _make_no_sentinel_vrt(tmp_path, 'no_sentinel_chunked_2180') - r = read_vrt(vrt, dtype=np.float64, chunks=2) + r = _read_vrt(vrt, dtype=np.float64, chunks=2) assert r.dtype == np.float64 assert 'nodata' not in r.attrs assert 'masked_nodata' not in r.attrs @@ -927,7 +927,7 @@ def test_missing_sources_eager_surfaces_vrt_holes(tmp_path): f.write(vrt_xml) with warnings.catch_warnings(): warnings.simplefilter('ignore') - r = read_vrt(vrt_path, missing_sources='warn') + r = _read_vrt(vrt_path, missing_sources='warn') assert 'vrt_holes' in r.attrs holes = r.attrs['vrt_holes'] assert isinstance(holes, list) and len(holes) >= 1 @@ -966,7 +966,7 @@ def test_missing_sources_chunked_surfaces_vrt_holes(tmp_path): """ with open(vrt_path, 'w') as f: f.write(vrt_xml) - r = read_vrt(vrt_path, missing_sources='warn', chunks=2) + r = _read_vrt(vrt_path, missing_sources='warn', chunks=2) assert 'vrt_holes' in r.attrs holes = r.attrs['vrt_holes'] assert isinstance(holes, list) and len(holes) >= 1 @@ -998,7 +998,7 @@ def test_georef_status_eager_parity(tmp_path, pair_factory, expected_status, ``georef_status``.""" tiff, vrt = pair_factory(tmp_path, f'georef_eager_{expected_status}') kwargs = {'allow_rotated': True} if allow_rotated else {} - vrt_status = read_vrt(vrt, **kwargs).attrs.get('georef_status') + vrt_status = _read_vrt(vrt, **kwargs).attrs.get('georef_status') assert vrt_status == expected_status if not allow_rotated: tiff_status = open_geotiff(tiff, **kwargs).attrs.get('georef_status') @@ -1013,10 +1013,10 @@ def test_georef_status_chunked_parity(tmp_path, pair_factory, expected_status, """VRT chunked and non-VRT chunked agree on ``georef_status``.""" tiff, vrt = pair_factory(tmp_path, f'georef_chunked_{expected_status}') kwargs = {'allow_rotated': True} if allow_rotated else {} - vrt_status = read_vrt(vrt, chunks=2, **kwargs).attrs.get('georef_status') + vrt_status = _read_vrt(vrt, chunks=2, **kwargs).attrs.get('georef_status') assert vrt_status == expected_status if not allow_rotated: - tiff_status = read_geotiff_dask( + tiff_status = _read_geotiff_dask( tiff, chunks=2, **kwargs ).attrs.get('georef_status') assert tiff_status == expected_status @@ -1039,18 +1039,18 @@ def test_vrt_eager_chunked_internal_parity(tmp_path, pair_factory, canonical attrs (modulo the lazy ``nodata_pixels_present`` carve-out).""" _, vrt = pair_factory(tmp_path, 'internal_parity_2180') kwargs = {'allow_rotated': True} if allow_rotated else {} - eager_attrs = dict(read_vrt(vrt, **kwargs).attrs) - chunked_attrs = dict(read_vrt(vrt, chunks=2, **kwargs).attrs) + eager_attrs = dict(_read_vrt(vrt, **kwargs).attrs) + chunked_attrs = dict(_read_vrt(vrt, chunks=2, **kwargs).attrs) eager_attrs.pop('nodata_pixels_present', None) chunked_attrs.pop('nodata_pixels_present', None) assert eager_attrs == chunked_attrs # =========================================================================== -# read_vrt backend / parameter coverage +# _read_vrt backend / parameter coverage # =========================================================================== # -# Covers the GPU and dask+GPU decode paths the read_vrt body handles, the +# Covers the GPU and dask+GPU decode paths the _read_vrt body handles, the # ``dtype=`` / ``name=`` kwargs, and the open_geotiff file-like + # backend-kwarg rejection. @@ -1068,27 +1068,27 @@ def single_tile_vrt(tmp_path): @_gpu_only class TestReadVrtGpuBackend: - """``read_vrt(gpu=True)`` returns a CuPy-backed DataArray.""" + """``_read_vrt(gpu=True)`` returns a CuPy-backed DataArray.""" def test_read_vrt_gpu_returns_cupy(self, single_tile_vrt): import cupy vrt_path, arr = single_tile_vrt - da = read_vrt(vrt_path, gpu=True) + da = _read_vrt(vrt_path, gpu=True) assert isinstance(da.data, cupy.ndarray), ( f"expected cupy.ndarray, got {type(da.data).__name__}" ) np.testing.assert_array_equal(da.data.get(), arr) def test_read_vrt_gpu_chunks_returns_dask_cupy(self, single_tile_vrt): - """``read_vrt(gpu=True, chunks=N)`` is the dask+cupy VRT entry + """``_read_vrt(gpu=True, chunks=N)`` is the dask+cupy VRT entry point; the trailing ``result.chunk(...)`` block wraps the cupy backing without falling back to numpy.""" import cupy import dask.array as da_mod vrt_path, arr = single_tile_vrt - result = read_vrt(vrt_path, gpu=True, chunks=2) + result = _read_vrt(vrt_path, gpu=True, chunks=2) assert isinstance(result.data, da_mod.Array), ( f"expected dask Array, got {type(result.data).__name__}" @@ -1105,7 +1105,7 @@ def test_read_vrt_gpu_chunks_returns_dask_cupy(self, single_tile_vrt): np.testing.assert_array_equal(computed.data.get(), arr) def test_open_geotiff_vrt_gpu_routes_through(self, single_tile_vrt): - """``open_geotiff('.vrt', gpu=True)`` dispatches to ``read_vrt`` + """``open_geotiff('.vrt', gpu=True)`` dispatches to ``_read_vrt`` and surfaces the cupy data unchanged.""" import cupy @@ -1132,12 +1132,12 @@ def test_open_geotiff_vrt_gpu_chunks(self, single_tile_vrt): class TestReadVrtDtypeKwarg: - """``read_vrt(dtype=...)`` casts after decode and validates the cast.""" + """``_read_vrt(dtype=...)`` casts after decode and validates the cast.""" def test_safe_widening_cast(self, single_tile_vrt): """float32 -> float64 is permitted; values survive bit-for-bit.""" vrt_path, arr = single_tile_vrt - da = read_vrt(vrt_path, dtype='float64') + da = _read_vrt(vrt_path, dtype='float64') assert da.dtype == np.float64 np.testing.assert_array_equal(da.values, arr.astype(np.float64)) @@ -1145,20 +1145,20 @@ def test_float_to_int_rejected(self, single_tile_vrt): """Float-to-int is lossy and refused with a descriptive error.""" vrt_path, _ = single_tile_vrt with pytest.raises(ValueError, match="Cannot cast float"): - read_vrt(vrt_path, dtype='int32') + _read_vrt(vrt_path, dtype='int32') class TestReadVrtNameKwarg: - """``read_vrt(name='custom')`` overrides the file-stem derivation.""" + """``_read_vrt(name='custom')`` overrides the file-stem derivation.""" def test_explicit_name_used(self, single_tile_vrt): vrt_path, _ = single_tile_vrt - da = read_vrt(vrt_path, name='custom_name') + da = _read_vrt(vrt_path, name='custom_name') assert da.name == 'custom_name' def test_default_name_from_stem(self, single_tile_vrt): vrt_path, _ = single_tile_vrt - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) assert da.name == os.path.splitext(os.path.basename(vrt_path))[0] diff --git a/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py b/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py index ffd162485..14096ce18 100644 --- a/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py +++ b/xrspatial/geotiff/tests/vrt/test_source_opt_ins_2672.py @@ -1,6 +1,6 @@ """VRT source-read opt-in forwarding (issue #2672). -``read_vrt`` accepts ``allow_rotated`` and ``allow_invalid_nodata`` and +``_read_vrt`` accepts ``allow_rotated`` and ``allow_invalid_nodata`` and documents them as opt-ins, but until issue #2672 the eager and chunked paths only forwarded the codec flags to the per-source GeoTIFF read. A caller who passed ``allow_rotated=True`` or ``allow_invalid_nodata=True`` @@ -20,7 +20,7 @@ import numpy as np import pytest -from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, read_vrt +from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, _read_vrt from xrspatial.geotiff._errors import InvalidIntegerNodataError, RotatedTransformError # Reuse the existing hand-rolled TIFF builders so this suite shares one @@ -76,12 +76,12 @@ def _invalid_nodata_source_vrt(tmp_path) -> str: def test_eager_invalid_nodata_rejected_without_opt_in(tmp_path): vrt = _invalid_nodata_source_vrt(tmp_path) with pytest.raises(InvalidIntegerNodataError): - read_vrt(vrt) + _read_vrt(vrt) def test_eager_invalid_nodata_accepted_with_opt_in(tmp_path): vrt = _invalid_nodata_source_vrt(tmp_path) - da = read_vrt(vrt, allow_invalid_nodata=True) + da = _read_vrt(vrt, allow_invalid_nodata=True) # NaN sentinel can't match any uint16 pixel, so the dtype survives # and the literal pixels come through unmasked. assert da.dtype == np.uint16 @@ -110,12 +110,12 @@ def test_chunked_invalid_nodata_accepted_with_opt_in(tmp_path): def test_eager_rotated_source_rejected_without_opt_in(tmp_path): vrt = _rotated_source_vrt(tmp_path) with pytest.raises(RotatedTransformError): - read_vrt(vrt) + _read_vrt(vrt) def test_eager_rotated_source_accepted_with_opt_in(tmp_path): vrt = _rotated_source_vrt(tmp_path) - da = read_vrt(vrt, allow_rotated=True) + da = _read_vrt(vrt, allow_rotated=True) # The source pixel grid is read without the georef assumption. np.testing.assert_array_equal(da.values, [[10, 20], [30, 40]]) @@ -153,14 +153,14 @@ def test_missing_sources_warn_opt_in_avoids_false_hole(tmp_path): # hole and emits the fallback warning. with warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') - da_hole = read_vrt(vrt, missing_sources='warn') + da_hole = _read_vrt(vrt, missing_sources='warn') assert any(issubclass(w.category, GeoTIFFFallbackWarning) for w in caught) assert da_hole.attrs.get('vrt_holes') # With the opt-in forwarded, the source reads and no hole is recorded. with warnings.catch_warnings(record=True) as caught: warnings.simplefilter('always') - da_ok = read_vrt( + da_ok = _read_vrt( vrt, missing_sources='warn', allow_invalid_nodata=True) assert not any( issubclass(w.category, GeoTIFFFallbackWarning) for w in caught) diff --git a/xrspatial/geotiff/tests/vrt/test_validation.py b/xrspatial/geotiff/tests/vrt/test_validation.py index bcf835adc..c4f497959 100644 --- a/xrspatial/geotiff/tests/vrt/test_validation.py +++ b/xrspatial/geotiff/tests/vrt/test_validation.py @@ -9,7 +9,7 @@ * end-to-end negative coverage (warped, nested, mixed CRS, mixed dtype, mixed band count, mask, resample alg) through both public entry points. -* narrowed-``except`` contract in ``read_vrt`` for source-read failures +* narrowed-``except`` contract in ``_read_vrt`` for source-read failures (warn-and-continue vs propagate) under default and ``XRSPATIAL_GEOTIFF_STRICT=1`` modes. * path-traversal rejection in ``parse_vrt`` / ``_read_vrt_internal`` @@ -35,7 +35,7 @@ import xarray as xr from xrspatial.geotiff import GeoTIFFFallbackWarning, open_geotiff, to_geotiff -from xrspatial.geotiff._backends.vrt import read_vrt as _package_read_vrt +from xrspatial.geotiff._backends.vrt import _read_vrt as _package_read_vrt from xrspatial.geotiff._errors import (GeoTIFFAmbiguousMetadataError, MixedBandMetadataError, RotatedTransformError, UnparseableCRSError, UnsupportedGeoTIFFFeatureError, VRTUnsupportedError) @@ -44,10 +44,10 @@ from xrspatial.geotiff._vrt_validation import validate_parsed_vrt, validate_vrt_capability from xrspatial.geotiff._writer import write -# ``xrspatial.geotiff.read_vrt`` (re-exported from the package init) is the -# same callable as ``_backends.vrt.read_vrt``; the parametrise IDs below -# label the backend-module path as the "package" entry point because that -# is what the public alias resolves to. +# ``_backends.vrt._read_vrt`` is the private VRT reader that ``open_geotiff`` +# dispatches to for ``.vrt`` sources; the parametrise IDs below label the +# backend-module path as the "package" entry point because that is what the +# dispatcher resolves to. # --------------------------------------------------------------------------- @@ -489,8 +489,8 @@ def test_nested_vrt_uppercase_extension_rejected(tmp_path): @pytest.mark.parametrize( "reader", [ - pytest.param(_package_read_vrt, id="entry[package-read_vrt]"), - pytest.param(_internal_read_vrt, id="entry[internal-read_vrt]"), + pytest.param(_package_read_vrt, id="entry[package-_read_vrt]"), + pytest.param(_internal_read_vrt, id="entry[internal-_read_vrt]"), pytest.param(open_geotiff, id="entry[open_geotiff]"), ], ) @@ -565,8 +565,8 @@ def test_warp_options_rejected_at_parse(tmp_path, xml, scope): @pytest.mark.parametrize( "reader", [ - pytest.param(_package_read_vrt, id="entry[package-read_vrt]"), - pytest.param(_internal_read_vrt, id="entry[internal-read_vrt]"), + pytest.param(_package_read_vrt, id="entry[package-_read_vrt]"), + pytest.param(_internal_read_vrt, id="entry[internal-_read_vrt]"), ], ) def test_warp_options_dataset_rejected_via_entry_points(tmp_path, reader): @@ -681,8 +681,8 @@ def test_use_mask_band_non_canonical_truthy_accepted(tmp_path, flag): @pytest.mark.parametrize( "reader", [ - pytest.param(_package_read_vrt, id="entry[package-read_vrt]"), - pytest.param(_internal_read_vrt, id="entry[internal-read_vrt]"), + pytest.param(_package_read_vrt, id="entry[package-_read_vrt]"), + pytest.param(_internal_read_vrt, id="entry[internal-_read_vrt]"), ], ) def test_use_mask_band_rejected_via_entry_points(tmp_path, reader): @@ -720,7 +720,7 @@ def test_per_source_mask_band_message_names_source(tmp_path): # --------------------------------------------------------------------------- -# Entry-point parity: ``read_vrt`` (package) and ``open_geotiff`` produce +# Entry-point parity: ``_read_vrt`` (package) and ``open_geotiff`` produce # the same typed exception with the same message for the same bad input. # --------------------------------------------------------------------------- @@ -818,7 +818,7 @@ def test_mixed_source_band_count_rejected(tmp_path): @pytest.mark.parametrize( "reader", [ - pytest.param(_package_read_vrt, id="entry[package-read_vrt]"), + pytest.param(_package_read_vrt, id="entry[package-_read_vrt]"), pytest.param(open_geotiff, id="entry[open_geotiff]"), ], ) @@ -1103,7 +1103,7 @@ def test_supported_simple_vrt_round_trips_via_open_geotiff(tmp_path): # --------------------------------------------------------------------------- -# Reader-error narrowing: ``read_vrt`` historically ``except Exception``-ed +# Reader-error narrowing: ``_read_vrt`` historically ``except Exception``-ed # every source read, swallowing real bugs. The catch is now # narrowed to I/O / parse / codec-decode errors only. # @@ -1152,7 +1152,7 @@ def _write_simple_vrt(tmp_path, src_path, *, name: str | None = None): def _patch_read_to_array(monkeypatch, exc): """Make ``_reader.read_to_array`` raise ``exc`` on every call. - ``read_vrt`` does a local ``from ._reader import read_to_array`` + ``_read_vrt`` does a local ``from ._reader import read_to_array`` inside the function body, so patching the source attribute is enough -- the import picks up the stub at call time.""" from xrspatial.geotiff import _reader @@ -1210,7 +1210,7 @@ def test_narrow_except_io_or_parse_warns_in_default_mode( """I/O and parse failures warn-and-continue when ``missing_sources='warn'`` is opted in. The warning message names the source and the underlying exception type.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('narrow')}.tif" src_path.write_bytes(b'placeholder') @@ -1220,7 +1220,7 @@ def test_narrow_except_io_or_parse_warns_in_default_mode( with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - da = read_vrt(str(vrt_path), missing_sources='warn') + da = _read_vrt(str(vrt_path), missing_sources='warn') assert da.shape == (4, 4) fallback = [ @@ -1240,7 +1240,7 @@ def test_narrow_except_io_or_parse_reraises_in_strict_mode( set_strict_env, monkeypatch, tmp_path, exc, _expected_type_name, ): """Strict mode re-raises all I/O / parse / codec-decode failures.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('strict')}.tif" src_path.write_bytes(b'placeholder') @@ -1249,7 +1249,7 @@ def test_narrow_except_io_or_parse_reraises_in_strict_mode( _patch_read_to_array(monkeypatch, exc) with pytest.raises(type(exc)): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) @pytest.mark.parametrize( @@ -1265,7 +1265,7 @@ def test_narrow_except_bug_classes_propagate_in_default_mode( """Non-I/O bugs (``RuntimeError`` here as a stand-in, plus ``MemoryError``) must propagate even in default mode -- they are real failures, not "unreadable source" cases.""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('bug')}.tif" src_path.write_bytes(b'placeholder') @@ -1274,7 +1274,7 @@ def test_narrow_except_bug_classes_propagate_in_default_mode( _patch_read_to_array(monkeypatch, exc) with pytest.raises(type(exc)): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) def test_narrow_except_runtime_error_propagates_in_strict_mode( @@ -1282,7 +1282,7 @@ def test_narrow_except_runtime_error_propagates_in_strict_mode( ): """Strict mode propagates non-I/O bugs too (double-checks the runtime-error case under the strict flag).""" - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('bug_strict')}.tif" src_path.write_bytes(b'placeholder') @@ -1291,7 +1291,7 @@ def test_narrow_except_runtime_error_propagates_in_strict_mode( _patch_read_to_array(monkeypatch, RuntimeError("synthetic strict")) with pytest.raises(RuntimeError, match='synthetic strict'): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) @pytest.mark.skipif(not _has_zstandard(), @@ -1304,7 +1304,7 @@ def test_narrow_except_zstd_error_warns_in_default_mode( single corrupt ZSTD tile does not abort the whole mosaic.""" from zstandard import ZstdError - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('zstd')}.tif" src_path.write_bytes(b'placeholder') @@ -1314,7 +1314,7 @@ def test_narrow_except_zstd_error_warns_in_default_mode( with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - da = read_vrt(str(vrt_path), missing_sources='warn') + da = _read_vrt(str(vrt_path), missing_sources='warn') assert da.shape == (4, 4) fallback = [ @@ -1333,7 +1333,7 @@ def test_narrow_except_zstd_error_reraises_in_strict_mode( ): from zstandard import ZstdError - from xrspatial.geotiff import read_vrt + from xrspatial.geotiff import _read_vrt src_path = tmp_path / f"src_{_uniq('zstd_strict')}.tif" src_path.write_bytes(b'placeholder') @@ -1342,7 +1342,7 @@ def test_narrow_except_zstd_error_reraises_in_strict_mode( _patch_read_to_array(monkeypatch, ZstdError("synthetic zstd strict")) with pytest.raises(ZstdError, match='synthetic zstd strict'): - read_vrt(str(vrt_path)) + _read_vrt(str(vrt_path)) # --------------------------------------------------------------------------- @@ -1739,7 +1739,7 @@ def test_negative_srcrect_raises_under_strict_mode( # --------------------------------------------------------------------------- # # ``open_geotiff`` documents ``overview_level`` and ``on_gpu_failure`` but -# the VRT dispatch branch routes to ``read_vrt`` whose signature accepts +# the VRT dispatch branch routes to ``_read_vrt`` whose signature accepts # neither, so the kwargs were silently dropped. The fix refuses the # unsupported combinations up front. @@ -1771,9 +1771,9 @@ def _kwarg_drop_small_vrt(tmp_path): tile_b = tmp_path / "tile_b.tif" to_geotiff(da_b, str(tile_b)) - from xrspatial.geotiff import write_vrt + from xrspatial.geotiff import build_vrt vrt_path = tmp_path / "mosaic.vrt" - write_vrt(str(vrt_path), [str(tile_a), str(tile_b)]) + build_vrt(str(vrt_path), [str(tile_a), str(tile_b)]) return str(vrt_path) @@ -1806,7 +1806,7 @@ def test_rejects_on_gpu_failure_with_gpu_true(self, _kwarg_drop_small_vrt): def test_without_unsupported_kwargs_still_works(self, _kwarg_drop_small_vrt): """The previously-accepted kwargs still flow through to - ``read_vrt``.""" + ``_read_vrt``.""" da = open_geotiff(_kwarg_drop_small_vrt) assert da.shape == (4, 8) diff --git a/xrspatial/geotiff/tests/vrt/test_window.py b/xrspatial/geotiff/tests/vrt/test_window.py index 47f2a116f..79073da14 100644 --- a/xrspatial/geotiff/tests/vrt/test_window.py +++ b/xrspatial/geotiff/tests/vrt/test_window.py @@ -31,7 +31,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import read_vrt, to_geotiff +from xrspatial.geotiff import _read_vrt, to_geotiff from xrspatial.geotiff._reader import PixelSafetyLimitError, read_to_array from xrspatial.geotiff._vrt import _resample_nearest from xrspatial.geotiff._vrt import read_vrt as _dstrect_cap_read_vrt_internal @@ -447,7 +447,7 @@ def test_per_source_cap_error_includes_gb_hint(monkeypatch): duration of the read. That keeps the test honest about which branch's format string is being asserted. """ - # ``read_vrt`` imports ``_check_dimensions`` from ``_reader`` at + # ``_read_vrt`` imports ``_check_dimensions`` from ``_reader`` at # call time, so patching the reader module's binding takes effect # on the next call. from xrspatial.geotiff import _reader as reader_mod @@ -758,7 +758,7 @@ def test_tiny_vrt_with_huge_srcrect_now_reads_minimally(tmp_path): f' \n' f'\n' ) - arr = read_vrt(str(vrt), max_pixels=1) + arr = _read_vrt(str(vrt), max_pixels=1) assert arr.shape == (1, 1) @@ -786,7 +786,7 @@ def test_source_cap_still_fires_when_sub_window_exceeds_budget(tmp_path): f'\n' ) with pytest.raises(ValueError, match='exceed|safety limit'): - read_vrt(str(vrt), max_pixels=4) + _read_vrt(str(vrt), max_pixels=4) # --------------------------------------------------------------------------- @@ -853,14 +853,14 @@ def lazy_chunks_multiband_vrt(): def test_chunks_builds_dask_array_with_multiple_blocks(lazy_chunks_two_by_two_vrt): - """``read_vrt(chunks=(N,N))`` returns a dask-backed DataArray + """``_read_vrt(chunks=(N,N))`` returns a dask-backed DataArray whose underlying array has more than one chunk along each spatial axis. Before the fix the array was numpy-backed under ``result.chunk()``, so this asserts the new lazy graph is in play. """ vrt_path, _ = lazy_chunks_two_by_two_vrt - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) assert isinstance(result.data, da.Array), f'expected dask Array, got {type(result.data).__name__}' # noqa: E501 assert result.data.numblocks == (4, 4), f'expected 4x4 blocks, got {result.data.numblocks}' @@ -878,7 +878,7 @@ def counting_read(*args, **kwargs): counter['calls'] += 1 return real_read(*args, **kwargs) monkeypatch.setattr(vrt_module, 'read_vrt', counting_read) - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) assert counter['calls'] == 0, f"_read_vrt_internal called {counter['calls']} times before .compute(); the chunked path leaked an eager decode" # noqa: E501 computed = result.compute() assert counter['calls'] == 16, f"expected 16 per-chunk decodes after compute, got {counter['calls']}" # noqa: E501 @@ -887,8 +887,8 @@ def counting_read(*args, **kwargs): def test_chunked_compute_matches_eager(lazy_chunks_two_by_two_vrt): vrt_path, _ = lazy_chunks_two_by_two_vrt - eager = read_vrt(vrt_path) - chunked = read_vrt(vrt_path, chunks=(64, 64)).compute() + eager = _read_vrt(vrt_path) + chunked = _read_vrt(vrt_path, chunks=(64, 64)).compute() assert eager.shape == chunked.shape assert np.array_equal(eager.values, chunked.values), 'chunked compute diverged from eager read' np.testing.assert_array_equal(eager['x'].values, chunked['x'].values) @@ -903,8 +903,8 @@ def test_chunked_single_tile_matches_eager(lazy_chunks_single_tile_vrt): same single source. """ vrt_path, _ = lazy_chunks_single_tile_vrt - eager = read_vrt(vrt_path) - chunked = read_vrt(vrt_path, chunks=(32, 32)).compute() + eager = _read_vrt(vrt_path) + chunked = _read_vrt(vrt_path, chunks=(32, 32)).compute() assert np.array_equal(eager.values, chunked.values) @@ -915,7 +915,7 @@ def test_chunks_task_cap_raises(lazy_chunks_two_by_two_vrt): """ vrt_path, _ = lazy_chunks_two_by_two_vrt with pytest.raises(ValueError, match='chunks=.*task'): - read_vrt(vrt_path, chunks=(1, 1)) + _read_vrt(vrt_path, chunks=(1, 1)) def test_window_plus_chunks_matches_eager(lazy_chunks_two_by_two_vrt): @@ -925,8 +925,8 @@ def test_window_plus_chunks_matches_eager(lazy_chunks_two_by_two_vrt): """ vrt_path, _ = lazy_chunks_two_by_two_vrt window = (32, 48, 160, 192) - eager = read_vrt(vrt_path, window=window) - chunked = read_vrt(vrt_path, window=window, chunks=(64, 64)) + eager = _read_vrt(vrt_path, window=window) + chunked = _read_vrt(vrt_path, window=window, chunks=(64, 64)) assert isinstance(chunked.data, da.Array) assert chunked.data.numblocks == (2, 3), f'expected (2, 3) numblocks over the window, got {chunked.data.numblocks}' # noqa: E501 computed = chunked.compute() @@ -936,13 +936,13 @@ def test_window_plus_chunks_matches_eager(lazy_chunks_two_by_two_vrt): @pytest.mark.skipif(not _HAS_GPU, reason='cupy + CUDA required') def test_gpu_plus_chunks_returns_dask_on_cupy(lazy_chunks_two_by_two_vrt): - """``read_vrt(gpu=True, chunks=...)`` must build a dask graph whose + """``_read_vrt(gpu=True, chunks=...)`` must build a dask graph whose blocks are cupy-backed (not numpy that gets cupy-wrapped at compute time on the host). """ import cupy vrt_path, _ = lazy_chunks_two_by_two_vrt - result = read_vrt(vrt_path, gpu=True, chunks=(64, 64)) + result = _read_vrt(vrt_path, gpu=True, chunks=(64, 64)) assert isinstance(result.data, da.Array) assert isinstance(result.data._meta, cupy.ndarray), f'expected cupy _meta, got {type(result.data._meta).__module__}.{type(result.data._meta).__name__}' # noqa: E501 computed = result.compute() @@ -954,7 +954,7 @@ def test_multiband_plus_chunks_preserves_band_dim(lazy_chunks_multiband_vrt): every block and the assembled DataArray. """ vrt_path, src = lazy_chunks_multiband_vrt - result = read_vrt(vrt_path, chunks=(32, 32)) + result = _read_vrt(vrt_path, chunks=(32, 32)) assert isinstance(result.data, da.Array) assert result.dims == ('y', 'x', 'band') assert result.shape == (64, 64, 3) @@ -990,7 +990,7 @@ def test_chunked_propagates_vrt_holes_when_source_missing(lazy_chunks_two_by_two os.unlink(tile_files[0]) with warnings.catch_warnings(): warnings.simplefilter('ignore', GeoTIFFFallbackWarning) - result = read_vrt(vrt_path, chunks=(64, 64), missing_sources='warn') + result = _read_vrt(vrt_path, chunks=(64, 64), missing_sources='warn') assert 'vrt_holes' in result.attrs, 'chunked path dropped vrt_holes contract from #1734' holes = result.attrs['vrt_holes'] assert isinstance(holes, list) and len(holes) >= 1 @@ -1005,7 +1005,7 @@ def test_chunked_no_vrt_holes_attr_when_complete(lazy_chunks_two_by_two_vrt): ``attrs['vrt_holes']`` (eager parity: empty hole list is omitted). """ vrt_path, _ = lazy_chunks_two_by_two_vrt - result = read_vrt(vrt_path, chunks=(64, 64)) + result = _read_vrt(vrt_path, chunks=(64, 64)) assert 'vrt_holes' not in result.attrs @@ -1025,7 +1025,7 @@ def test_chunked_integer_no_nodata_keeps_source_dtype(): to_geotiff(raster, tile_path) vrt_path = os.path.join(td, 'mosaic.vrt') _write_vrt_internal(vrt_path, [tile_path]) - result = read_vrt(vrt_path, chunks=(32, 32)) + result = _read_vrt(vrt_path, chunks=(32, 32)) assert result.dtype == np.uint16, f'expected uint16 (source dtype), got {result.dtype}; chunked path promoted to float64 despite no declared nodata' # noqa: E501 computed = result.compute() assert computed.dtype == np.uint16 @@ -1090,7 +1090,7 @@ def test_vrt_chunked_dataset_is_shared_graph_input(tmp_path): """ from xrspatial.geotiff._vrt import VRTDataset vrt_path, n_sources = _chunked_shared_dataset_make_tile_vrt(str(tmp_path), n_tiles_per_side=4) - result = read_vrt(vrt_path, chunks=32) + result = _read_vrt(vrt_path, chunks=32) graph = result.__dask_graph__() assert n_sources == 16, 'fixture build sanity check' chunk_task_count = 0 @@ -1115,8 +1115,8 @@ def test_vrt_chunked_dataset_is_shared_graph_input(tmp_path): def test_vrt_chunked_decode_unchanged_after_shared_wrap(tmp_path): """The shared-Delayed wrap must not change decoded pixel values.""" vrt_path, _ = _chunked_shared_dataset_make_tile_vrt(str(tmp_path), n_tiles_per_side=3) - eager = read_vrt(vrt_path) - chunked = read_vrt(vrt_path, chunks=32).compute() + eager = _read_vrt(vrt_path) + chunked = _read_vrt(vrt_path, chunks=32).compute() np.testing.assert_array_equal(np.asarray(eager), np.asarray(chunked)) @@ -1124,7 +1124,7 @@ def test_vrt_chunked_band_kwarg_still_validates(tmp_path): """Wrapping the dataset must not change band validation behaviour.""" vrt_path, _ = _chunked_shared_dataset_make_tile_vrt(str(tmp_path), n_tiles_per_side=2) with pytest.raises(ValueError): - read_vrt(vrt_path, chunks=32, band=5) + _read_vrt(vrt_path, chunks=32, band=5) # --------------------------------------------------------------------------- @@ -1201,11 +1201,11 @@ def _write_and_collect(vrt_path: str) -> dict[str, bytes]: # # Two further windowed / chunked read paths this module covers: # -# * read_vrt(chunks=...) lazy-window construction: chunk layout +# * _read_vrt(chunks=...) lazy-window construction: chunk layout # matches eager values, build does not decode sources, and an # excessive task count is rejected. -# * read_geotiff_dask('.vrt') kwarg forwarding: the direct dask -# entry point forwards window / band / max_pixels through to read_vrt. +# * _read_geotiff_dask('.vrt') kwarg forwarding: the direct dask +# entry point forwards window / band / max_pixels through to _read_vrt. def _vrttail_write_single_band_vrt(vrt_path, source_name): @@ -1257,8 +1257,8 @@ def test_chunks_matches_eager_values(self, tmp_path): vrt = tmp_path / "tmp_1798_source.vrt" _vrttail_write_single_band_vrt(vrt, os.path.basename(src)) - eager = read_vrt(str(vrt)) - lazy = read_vrt(str(vrt), chunks=2) + eager = _read_vrt(str(vrt)) + lazy = _read_vrt(str(vrt), chunks=2) assert lazy.data.chunks == ((2, 2), (2, 2, 2)) np.testing.assert_array_equal(lazy.compute().values, eager.values) @@ -1276,7 +1276,7 @@ def test_chunks_does_not_read_sources_during_construction(self, tmp_path): _vrttail_write_single_band_vrt(vrt, "missing.tif") with warnings.catch_warnings(record=True) as caught: - lazy = read_vrt(str(vrt), chunks=2, missing_sources="warn") + lazy = _read_vrt(str(vrt), chunks=2, missing_sources="warn") assert caught == [] assert hasattr(lazy.data, 'compute') @@ -1289,14 +1289,14 @@ def test_chunks_rejects_excessive_task_count(self, tmp_path): '\n' ) with pytest.raises(ValueError, match="task cap"): - read_vrt(str(vrt), chunks=1, max_pixels=20_000_000_000) + _read_vrt(str(vrt), chunks=1, max_pixels=20_000_000_000) class TestVrtTailDirectDaskKwargs: - """read_geotiff_dask('.vrt') forwards VRT kwargs.""" + """_read_geotiff_dask('.vrt') forwards VRT kwargs.""" def test_forwards_window_and_band(self, tmp_path): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask arr = np.arange(4 * 6 * 2, dtype=np.float32).reshape(4, 6, 2) src = tmp_path / "tmp_1797_source.tif" @@ -1304,14 +1304,14 @@ def test_forwards_window_and_band(self, tmp_path): vrt = tmp_path / "tmp_1797_source.vrt" _vrttail_write_multi_band_vrt(vrt, os.path.basename(src), bands=2) - got = read_geotiff_dask( + got = _read_geotiff_dask( str(vrt), chunks=2, window=(1, 2, 4, 6), band=1, ) assert got.shape == (3, 4) np.testing.assert_array_equal(got.values, arr[1:4, 2:6, 1]) def test_forwards_max_pixels(self, tmp_path): - from xrspatial.geotiff import read_geotiff_dask + from xrspatial.geotiff import _read_geotiff_dask arr = np.arange(24, dtype=np.float32).reshape(4, 6) src = tmp_path / "tmp_1797_source_cap.tif" @@ -1320,4 +1320,4 @@ def test_forwards_max_pixels(self, tmp_path): _vrttail_write_single_band_vrt(vrt, os.path.basename(src)) with pytest.raises(ValueError, match="exceed"): - read_geotiff_dask(str(vrt), chunks=2, max_pixels=10) + _read_geotiff_dask(str(vrt), chunks=2, max_pixels=10) diff --git a/xrspatial/geotiff/tests/write/test_basic.py b/xrspatial/geotiff/tests/write/test_basic.py index 09a79819b..76b8b125b 100644 --- a/xrspatial/geotiff/tests/write/test_basic.py +++ b/xrspatial/geotiff/tests/write/test_basic.py @@ -1,6 +1,6 @@ """Generic writer paths. -Covers the eager ``to_geotiff`` / ``write_geotiff_gpu`` / ``write_vrt`` +Covers the eager ``to_geotiff`` / ``_write_geotiff_gpu`` / ``build_vrt`` surface: round-trip basics, dtype x compression matrix, kwarg order and return-path contracts, the uncompressed-tiled no-dead-alloc gate, the writer layout monkeypatch contract, and the VRT writer surface @@ -36,13 +36,13 @@ from xrspatial.geotiff import _vrt as _vrt_module from xrspatial.geotiff import _writer as writer_mod -from xrspatial.geotiff import open_geotiff, read_vrt, to_geotiff, write_geotiff_gpu, write_vrt +from xrspatial.geotiff import open_geotiff, _read_vrt, to_geotiff, _write_geotiff_gpu, build_vrt from xrspatial.geotiff._compression import COMPRESSION_NONE from xrspatial.geotiff._geotags import GeoTransform from xrspatial.geotiff._header import TAG_PHOTOMETRIC, parse_header, parse_ifd from xrspatial.geotiff._reader import _read_to_array, read_to_array from xrspatial.geotiff._validation import _validate_3d_writer_dims -# ``write_vrt`` here is the private internal binding, aliased so it does +# ``build_vrt`` here is the private internal binding, aliased so it does # not shadow the public re-export above. The only section that needs # the private form is the writer-source-compat fold (see PR # description for the why). @@ -396,7 +396,7 @@ def test_write_to_readonly_dir_raises_oserror(tmp_path): # ------------------------------------------------------------------------- def test_writer_kwarg_order_matches_to_geotiff(): - """``write_geotiff_gpu`` lists its kwargs in the same order as + """``_write_geotiff_gpu`` lists its kwargs in the same order as ``to_geotiff``, modulo the ``gpu`` kwarg the GPU writer omits. Both signatures use keyword-only kwargs so positional callers are @@ -404,9 +404,9 @@ def test_writer_kwarg_order_matches_to_geotiff(): docs, and any caller that inspects ``inspect.signature``. """ eager_params = list(inspect.signature(to_geotiff).parameters) - gpu_params = list(inspect.signature(write_geotiff_gpu).parameters) + gpu_params = list(inspect.signature(_write_geotiff_gpu).parameters) - # to_geotiff has ``gpu`` (auto-dispatch flag); write_geotiff_gpu does + # to_geotiff has ``gpu`` (auto-dispatch flag); _write_geotiff_gpu does # not. Drop it from the comparison instead of asserting on the # missing kwarg directly, so unrelated future additions to either # signature still surface here. @@ -415,10 +415,10 @@ def test_writer_kwarg_order_matches_to_geotiff(): eager_params_no_gpu = [p for p in eager_params if p != 'gpu'] assert gpu_params == eager_params_no_gpu, ( - "write_geotiff_gpu and to_geotiff kwarg order diverged.\n" + "_write_geotiff_gpu and to_geotiff kwarg order diverged.\n" f" to_geotiff (with 'gpu' removed): {eager_params_no_gpu}\n" - f" write_geotiff_gpu: {gpu_params}\n" - "Reorder write_geotiff_gpu to match to_geotiff (see #1922)." + f" _write_geotiff_gpu: {gpu_params}\n" + "Reorder _write_geotiff_gpu to match to_geotiff (see #1922)." ) @@ -426,12 +426,12 @@ def test_writer_kwarg_defaults_match_to_geotiff(): """The kwargs both writers share also have identical defaults. A surprise-free dispatch ``to_geotiff(..., gpu=True)`` requires - ``write_geotiff_gpu`` to default the same way for every kwarg the + ``_write_geotiff_gpu`` to default the same way for every kwarg the auto-dispatch entry point forwards (``allow_internal_only_jpeg`` was added to satisfy that contract; this test pins the broader parity). """ eager_sig = inspect.signature(to_geotiff) - gpu_sig = inspect.signature(write_geotiff_gpu) + gpu_sig = inspect.signature(_write_geotiff_gpu) shared = set(eager_sig.parameters) & set(gpu_sig.parameters) # ``data`` and ``path`` are required positionals with no default; @@ -443,7 +443,7 @@ def test_writer_kwarg_defaults_match_to_geotiff(): if ed != gd: mismatches.append((name, ed, gd)) assert not mismatches, ( - "write_geotiff_gpu and to_geotiff disagree on defaults: " + "_write_geotiff_gpu and to_geotiff disagree on defaults: " f"{mismatches}" ) @@ -525,12 +525,12 @@ def test_to_geotiff_dask_streaming_returns_path(tmp_path): def test_write_vrt_returns_string_path(tmp_path): - """``write_vrt`` (already conformant) keeps returning the str path.""" + """``build_vrt`` (already conformant) keeps returning the str path.""" # Create a source tif first. src = tmp_path / "src.tif" to_geotiff(_small_da(), str(src)) vrt_path = tmp_path / "out.vrt" - rv = write_vrt(str(vrt_path), [str(src)]) + rv = build_vrt(str(vrt_path), [str(src)]) assert isinstance(rv, str) assert rv == str(vrt_path) assert os.path.exists(rv) @@ -551,7 +551,7 @@ def test_write_geotiff_gpu_returns_string_path(tmp_path): attrs={"crs": 4326}, ) out = tmp_path / "test_1938_gpu.tif" - rv = write_geotiff_gpu(da, str(out)) + rv = _write_geotiff_gpu(da, str(out)) assert isinstance(rv, str) assert rv == str(out) assert os.path.exists(rv) @@ -565,8 +565,8 @@ def test_writer_signatures_declare_path_return(): """ expected = { to_geotiff: "str | BinaryIO", - write_geotiff_gpu: "str | BinaryIO", - write_vrt: "str", + _write_geotiff_gpu: "str | BinaryIO", + build_vrt: "str", } for fn, expected_ann in expected.items(): sig = inspect.signature(fn) @@ -579,7 +579,7 @@ def test_writer_signatures_declare_path_return(): def test_writer_returns_are_not_none(tmp_path): """None of the public writers may go back to returning ``None``.""" # Use the ``tmp_path`` fixture (not ``tempfile.TemporaryDirectory``) - # because ``write_vrt`` reads each source through the module-level + # because ``build_vrt`` reads each source through the module-level # ``_MmapCache`` in ``_reader.py``, which keeps the file handle and # mmap of ``src.tif`` open after ``_FileSource.close()`` so repeated # reads of the same file stay cheap. On Windows that cached handle @@ -594,7 +594,7 @@ def test_writer_returns_are_not_none(tmp_path): assert rv is not None src = str(tmp_path / "src.tif") to_geotiff(da, src) - vrt_rv = write_vrt(str(tmp_path / "m.vrt"), [src]) + vrt_rv = build_vrt(str(tmp_path / "m.vrt"), [src]) assert vrt_rv is not None @@ -771,7 +771,7 @@ def _wrapped(*args, **kwargs): # ------------------------------------------------------------------------- -# Section: write_vrt path kwarg contract +# Section: build_vrt path kwarg contract # ------------------------------------------------------------------------- def _build_source_tif(tmp_path, name='src.tif'): @@ -788,14 +788,14 @@ def _build_source_tif(tmp_path, name='src.tif'): def test_write_vrt_signature_first_arg_is_path(): - """Signature parity with to_geotiff / write_geotiff_gpu. + """Signature parity with to_geotiff / _write_geotiff_gpu. The api-consistency sweep cares specifically about ``inspect.signature``: IDE autocomplete, mypy, and Sphinx-rendered docs all read the same source. Pinning the first param name here catches any future re-rename that re-introduces the drift. """ - sig = inspect.signature(write_vrt) + sig = inspect.signature(build_vrt) params = list(sig.parameters) # ``path`` is the new canonical name, ``source_files`` follows. # ``vrt_path`` is kept as a keyword-only deprecated alias. @@ -808,9 +808,9 @@ def test_write_vrt_signature_first_arg_is_path(): def test_write_vrt_positional_path_works(tmp_path): - """Positional ``write_vrt(path, sources)`` is unchanged. + """Positional ``build_vrt(path, sources)`` is unchanged. - Existing callers ``write_vrt(some_path, sources)`` keep working + Existing callers ``build_vrt(some_path, sources)`` keep working after the rename because the new ``path`` parameter sits where ``vrt_path`` used to be. No deprecation warning should fire. """ @@ -818,24 +818,24 @@ def test_write_vrt_positional_path_works(tmp_path): out = str(tmp_path / 'out.vrt') with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - result = write_vrt(out, [src]) + result = build_vrt(out, [src]) assert result == out assert os.path.exists(out) def test_write_vrt_path_kwarg_works(tmp_path): - """Keyword ``write_vrt(path=..., source_files=...)`` works. + """Keyword ``build_vrt(path=..., source_files=...)`` works. A caller who passes everything by keyword (no positional args) previously could not reach the function because the ``path`` kwarg did not exist; this is the path-symmetric counterpart to the existing - ``write_vrt(vrt_path=...)`` test below. + ``build_vrt(vrt_path=...)`` test below. """ src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - result = write_vrt(path=out, source_files=[src]) + result = build_vrt(path=out, source_files=[src]) assert result == out assert os.path.exists(out) @@ -850,7 +850,7 @@ def test_write_vrt_vrt_path_kwarg_emits_deprecation_warning(tmp_path): src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') with pytest.warns(DeprecationWarning, match='vrt_path'): - result = write_vrt(vrt_path=out, source_files=[src]) + result = build_vrt(vrt_path=out, source_files=[src]) assert result == out assert os.path.exists(out) @@ -859,13 +859,13 @@ def test_write_vrt_path_and_vrt_path_together_raises(tmp_path): """Both names supplied is ambiguous; refuse to pick one. Mirrors the ``crs`` / ``crs_wkt`` rule documented in the existing - write_vrt source: passing both is rejected with TypeError + build_vrt source: passing both is rejected with TypeError regardless of whether the two values happen to match. """ src = _build_source_tif(tmp_path) out = str(tmp_path / 'out.vrt') with pytest.raises(TypeError, match="path.*vrt_path"): - write_vrt(path=out, vrt_path=out, source_files=[src]) + build_vrt(path=out, vrt_path=out, source_files=[src]) def test_write_vrt_no_path_raises(tmp_path): @@ -879,11 +879,11 @@ def test_write_vrt_no_path_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match='path'): - write_vrt(source_files=[src]) + build_vrt(source_files=[src]) def test_write_vrt_explicit_path_none_raises(tmp_path): - """``write_vrt(path=None, ...)`` is rejected with TypeError. + """``build_vrt(path=None, ...)`` is rejected with TypeError. The sentinel-default pattern distinguishes "caller passed nothing" (sentinel) from "caller passed None explicitly". @@ -893,11 +893,11 @@ def test_write_vrt_explicit_path_none_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match="'path'.*None"): - write_vrt(path=None, source_files=[src]) + build_vrt(path=None, source_files=[src]) def test_write_vrt_positional_none_raises(tmp_path): - """Positional ``write_vrt(None, sources)`` is rejected with TypeError. + """Positional ``build_vrt(None, sources)`` is rejected with TypeError. Same rationale as the keyword case: an explicit positional ``None`` is rejected up front instead of crashing deep in @@ -907,7 +907,7 @@ def test_write_vrt_positional_none_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match="'path'.*None"): - write_vrt(None, [src]) + build_vrt(None, [src]) def test_write_vrt_first_arg_name_matches_writer_trio(): @@ -922,10 +922,10 @@ def test_write_vrt_first_arg_name_matches_writer_trio(): inspect.signature(to_geotiff).parameters )[1] # data, path -> index 1 gpu_first = list( - inspect.signature(write_geotiff_gpu).parameters + inspect.signature(_write_geotiff_gpu).parameters )[1] vrt_first = list( - inspect.signature(write_vrt).parameters + inspect.signature(build_vrt).parameters )[0] # path, source_files -> index 0 assert eager_first == 'path' assert gpu_first == 'path' @@ -943,20 +943,20 @@ def test_write_vrt_path_round_trip_matches_old(tmp_path): out_new = str(tmp_path / 'out_new.vrt') out_old = str(tmp_path / 'out_old.vrt') - write_vrt(out_new, [src]) + build_vrt(out_new, [src]) with warnings.catch_warnings(): # ignore the deprecation; we still need the legacy path to # produce a byte-identical mosaic. warnings.simplefilter('ignore', DeprecationWarning) - write_vrt(vrt_path=out_old, source_files=[src]) + build_vrt(vrt_path=out_old, source_files=[src]) - a_new = read_vrt(out_new) - a_old = read_vrt(out_old) + a_new = _read_vrt(out_new) + a_old = _read_vrt(out_old) np.testing.assert_array_equal(np.asarray(a_new), np.asarray(a_old)) # ------------------------------------------------------------------------- -# Section: write_vrt CRS propagation +# Section: build_vrt CRS propagation # ------------------------------------------------------------------------- @@ -967,18 +967,18 @@ def test_write_vrt_accepts_crs_kwarg(): """``crs`` is in the signature and defaults to ``None``.""" import inspect - sig = inspect.signature(write_vrt) + sig = inspect.signature(build_vrt) assert 'crs' in sig.parameters assert sig.parameters['crs'].default is None def test_write_vrt_crs_annotation_matches_writer_trio(): """``crs`` is annotated ``int | str | None``, identical to - ``to_geotiff(..., crs=...)`` and ``write_geotiff_gpu(..., crs=...)``. + ``to_geotiff(..., crs=...)`` and ``_write_geotiff_gpu(..., crs=...)``. """ import inspect - sig = inspect.signature(write_vrt) + sig = inspect.signature(build_vrt) ann = str(sig.parameters['crs'].annotation) assert ann == 'int | str | None' @@ -991,18 +991,18 @@ def test_write_vrt_crs_epsg_int_writes_wkt_to_xml(tmp_path): The current implementation forwards the WKT to ``_vrt.write_vrt``, which interpolates it into the XML node. Reading the file - back with ``read_vrt`` must therefore produce + back with ``_read_vrt`` must therefore produce ``attrs['crs'] == 4326`` (because ``_wkt_to_epsg`` round-trips EPSG:4326's WKT cleanly). """ src = _build_source_tif(tmp_path, 'epsg_int.tif') vrt_path = str(tmp_path / 'epsg_int.vrt') - out = write_vrt(vrt_path, [src], crs=4326) + out = build_vrt(vrt_path, [src], crs=4326) assert out == vrt_path assert os.path.exists(vrt_path) - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) assert da.attrs.get('crs') == 4326 @@ -1016,9 +1016,9 @@ def test_write_vrt_crs_wkt_string(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() - out = write_vrt(vrt_path, [src], crs=wkt) + out = build_vrt(vrt_path, [src], crs=wkt) assert out == vrt_path - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) # WKT round-trips back to EPSG:4326 via _wkt_to_epsg assert da.attrs.get('crs') == 4326 @@ -1030,9 +1030,9 @@ def test_write_vrt_crs_none_falls_through(tmp_path): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - out = write_vrt(vrt_path, [src], crs=None) + out = build_vrt(vrt_path, [src], crs=None) assert out == vrt_path - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) # The source TIFF was written with EPSG:4326; VRT inherits it. assert da.attrs.get('crs') == 4326 @@ -1046,7 +1046,7 @@ def test_write_vrt_no_crs_kwarg_no_warning(tmp_path): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - write_vrt(vrt_path, [src]) # neither kwarg supplied + build_vrt(vrt_path, [src]) # neither kwarg supplied assert os.path.exists(vrt_path) @@ -1064,9 +1064,9 @@ def test_write_vrt_crs_wkt_deprecated_warns(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() with pytest.warns(DeprecationWarning, match='crs_wkt'): - out = write_vrt(vrt_path, [src], crs_wkt=wkt) + out = build_vrt(vrt_path, [src], crs_wkt=wkt) assert out == vrt_path - da = read_vrt(vrt_path) + da = _read_vrt(vrt_path) assert da.attrs.get('crs') == 4326 @@ -1078,7 +1078,7 @@ def test_write_vrt_crs_wkt_none_still_warns(tmp_path): vrt_path = str(tmp_path / 'depr_none.vrt') with pytest.warns(DeprecationWarning, match='crs_wkt'): - write_vrt(vrt_path, [src], crs_wkt=None) + build_vrt(vrt_path, [src], crs_wkt=None) assert os.path.exists(vrt_path) @@ -1094,7 +1094,7 @@ def test_write_vrt_both_crs_and_crs_wkt_rejected(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() with pytest.raises(TypeError, match='crs.*crs_wkt'): - write_vrt(vrt_path, [src], crs=4326, crs_wkt=wkt) + build_vrt(vrt_path, [src], crs=4326, crs_wkt=wkt) # --- Cross-writer parity: same kwarg name on all three writers --- @@ -1106,9 +1106,9 @@ def test_writer_trio_all_accept_crs_kwarg(): output extension never has to special-case the kwarg name.""" import inspect - from xrspatial.geotiff import to_geotiff, write_geotiff_gpu, write_vrt + from xrspatial.geotiff import to_geotiff, _write_geotiff_gpu, build_vrt - for fn in (to_geotiff, write_geotiff_gpu, write_vrt): + for fn in (to_geotiff, _write_geotiff_gpu, build_vrt): sig = inspect.signature(fn) assert 'crs' in sig.parameters, f"{fn.__name__} missing crs kwarg" assert ( @@ -1126,7 +1126,7 @@ def test_write_vrt_crs_invalid_type_rejected(tmp_path): vrt_path = str(tmp_path / 'bad_type.vrt') with pytest.raises(TypeError, match='crs must be'): - write_vrt(vrt_path, [src], crs=[4326]) + build_vrt(vrt_path, [src], crs=[4326]) def test_write_vrt_crs_unparseable_string_rejected(tmp_path): @@ -1137,11 +1137,11 @@ def test_write_vrt_crs_unparseable_string_rejected(tmp_path): vrt_path = str(tmp_path / 'bad_str.vrt') with pytest.raises(ValueError, match='Could not parse crs'): - write_vrt(vrt_path, [src], crs='not-a-real-crs-string') + build_vrt(vrt_path, [src], crs='not-a-real-crs-string') # ------------------------------------------------------------------------- -# Section: write_vrt bool nodata +# Section: build_vrt bool nodata # ------------------------------------------------------------------------- @pytest.fixture @@ -1153,14 +1153,14 @@ def uint8_da(): @pytest.fixture def src_geotiff(uint8_da, tmp_path): - """A real on-disk source GeoTIFF that write_vrt can point at.""" + """A real on-disk source GeoTIFF that build_vrt can point at.""" path = str(tmp_path / "src_1921.tif") to_geotiff(uint8_da, path) return path # --------------------------------------------------------------------------- -# write_vrt: bool nodata rejection +# build_vrt: bool nodata rejection # --------------------------------------------------------------------------- @@ -1169,15 +1169,15 @@ def src_geotiff(uint8_da, tmp_path): [True, False, np.bool_(True), np.bool_(False)], ) def test_write_vrt_rejects_bool_nodata(src_geotiff, tmp_path, bad): - """``write_vrt`` raises ``TypeError`` for any bool nodata. + """``build_vrt`` raises ``TypeError`` for any bool nodata. - The public ``write_vrt`` wrapper routes + The public ``build_vrt`` wrapper routes through ``_validate_nodata_arg`` and adds a defense-in-depth check inside the internal ``_vrt.write_vrt``. """ vrt_path = str(tmp_path / "out_1921_bad.vrt") with pytest.raises(TypeError, match="nodata must be numeric"): - write_vrt(vrt_path, [src_geotiff], nodata=bad) + build_vrt(vrt_path, [src_geotiff], nodata=bad) @pytest.mark.parametrize( @@ -1208,7 +1208,7 @@ def test_write_vrt_internal_rejects_bool_nodata(src_geotiff, tmp_path, bad): def test_write_vrt_accepts_numeric_nodata(src_geotiff, tmp_path, good): """Numeric sentinels go through unchanged: the fix must not over-reject.""" vrt_path = str(tmp_path / f"out_1921_numeric_{good!r}.vrt") - write_vrt(vrt_path, [src_geotiff], nodata=good) + build_vrt(vrt_path, [src_geotiff], nodata=good) with open(vrt_path) as f: content = f.read() # The exact format of the emitted nodata string is implementation @@ -1220,12 +1220,12 @@ def test_write_vrt_accepts_numeric_nodata(src_geotiff, tmp_path, good): def test_write_vrt_accepts_none_nodata(src_geotiff, tmp_path): """``nodata=None`` is the documented default and must keep working.""" vrt_path = str(tmp_path / "out_1921_none.vrt") - write_vrt(vrt_path, [src_geotiff], nodata=None) + build_vrt(vrt_path, [src_geotiff], nodata=None) assert os.path.exists(vrt_path) # --------------------------------------------------------------------------- -# write_geotiff_gpu: defense-in-depth parity +# _write_geotiff_gpu: defense-in-depth parity # --------------------------------------------------------------------------- @@ -1235,7 +1235,7 @@ def test_write_vrt_accepts_none_nodata(src_geotiff, tmp_path): [True, False, np.bool_(True), np.bool_(False)], ) def test_write_geotiff_gpu_rejects_bool_nodata(uint8_da, tmp_path, bad): - """Direct ``write_geotiff_gpu`` call rejects bool nodata. + """Direct ``_write_geotiff_gpu`` call rejects bool nodata. The top-of-function ``_validate_nodata_arg`` call fires first; the deeper ``build_geo_tags`` guard is a second line @@ -1243,10 +1243,10 @@ def test_write_geotiff_gpu_rejects_bool_nodata(uint8_da, tmp_path, bad): top-of-function call surfaces here, not deep inside the geotag builder. """ - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu path = str(tmp_path / "gpu_1921_bad.tif") with pytest.raises(TypeError, match="nodata must be numeric"): - write_geotiff_gpu(uint8_da, path, nodata=bad) + _write_geotiff_gpu(uint8_da, path, nodata=bad) @requires_gpu @@ -1263,7 +1263,7 @@ def test_to_geotiff_gpu_dispatch_rejects_bool_nodata(uint8_da, tmp_path): # ------------------------------------------------------------------------- -# Section: write_vrt int nodata +# Section: build_vrt int nodata # ------------------------------------------------------------------------- def _nodata_annotation(fn): @@ -1273,7 +1273,7 @@ def _nodata_annotation(fn): def test_write_vrt_public_nodata_accepts_int_annotation(): """The public wrapper widens the annotation to include int.""" - ann = _nodata_annotation(write_vrt) + ann = _nodata_annotation(build_vrt) # Allow either typing.Union[float, int, None] or float | int | None. if isinstance(ann, str): # Forward-referenced string annotation (rare here; defensive). @@ -1322,7 +1322,7 @@ def test_write_vrt_int_nodata_round_trips(tmp_path): vrt_path = tmp_path / "mosaic.vrt" # Passing an int sentinel must not raise; the surface should match # to_geotiff's "float, int, or None" contract. - write_vrt(str(vrt_path), [str(tif_path)], nodata=65535) + build_vrt(str(vrt_path), [str(tif_path)], nodata=65535) # Confirm the int round-trips through the parser back into a VRT band. parsed = _vrt_module.parse_vrt( @@ -2120,12 +2120,12 @@ def test_dask_streaming_rejects_ambiguous_3d(tmp_path, dims, shape): ]) def test_gpu_writer_rejects_ambiguous_3d(tmp_path, dims, shape): """GPU writer raises ValueError on ambiguous 3D dim names.""" - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu da = _make_da_1812(dims, shape, backend="cupy") out_path = tmp_path / f"tmp_1812_gpu_{'_'.join(dims)}.tif" with pytest.raises(ValueError, match="ambiguous dims|temporal leading dim"): - write_geotiff_gpu(da, str(out_path), crs=4326) + _write_geotiff_gpu(da, str(out_path), crs=4326) @pytest.mark.parametrize("dims, shape", _HAPPY_3D_INPUTS_1812) @@ -2216,13 +2216,13 @@ def test_gpu_writer_happy_path_still_works(tmp_path): """GPU writer's existing happy paths (band-first and band-last) survive.""" import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu arr_bf = cupy.arange(3 * 4 * 5, dtype=cupy.uint8).reshape(3, 4, 5) da_bf = xr.DataArray(arr_bf, dims=("band", "y", "x"), attrs={"crs": "EPSG:4326"}) p_bf = tmp_path / "tmp_1812_gpu_bf.tif" - write_geotiff_gpu(da_bf, str(p_bf), crs=4326) + _write_geotiff_gpu(da_bf, str(p_bf), crs=4326) out_bf = open_geotiff(str(p_bf)) assert out_bf.shape == (4, 5, 3) @@ -2230,7 +2230,7 @@ def test_gpu_writer_happy_path_still_works(tmp_path): da_bl = xr.DataArray(arr_bl, dims=("y", "x", "band"), attrs={"crs": "EPSG:4326"}) p_bl = tmp_path / "tmp_1812_gpu_bl.tif" - write_geotiff_gpu(da_bl, str(p_bl), crs=4326) + _write_geotiff_gpu(da_bl, str(p_bl), crs=4326) out_bl = open_geotiff(str(p_bl)) assert out_bl.shape == (4, 5, 3) @@ -2381,19 +2381,19 @@ def test_to_geotiff_rejects_empty_numpy(tmp_path, shape): @requires_gpu def test_write_geotiff_gpu_rejects_empty(tmp_path): - """``write_geotiff_gpu`` is a public entry point and does not go + """``_write_geotiff_gpu`` is a public entry point and does not go through ``to_geotiff``; make sure the empty-shape guard fires there too.""" import cupy as cp - from xrspatial.geotiff._writers.gpu import write_geotiff_gpu + from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu arr = cp.zeros((0, 5), dtype=cp.float32) out = tmp_path / "tmp_2075_empty_gpu_0x5.tif" with pytest.raises(ValueError) as excinfo: - write_geotiff_gpu(arr, str(out)) + _write_geotiff_gpu(arr, str(out)) msg = str(excinfo.value) - assert "write_geotiff_gpu" in msg + assert "_write_geotiff_gpu" in msg assert "height=0" in msg assert not out.exists() @@ -2478,7 +2478,7 @@ def test_write_band_last_zero_bands_direct(tmp_path): # assertion fails if the wrong entry point fires (every message # also contains the substring "write" further on, so an `in` # check would not distinguish ``write`` from ``write_streaming`` - # or ``write_geotiff_gpu``). + # or ``_write_geotiff_gpu``). # The array-level entry point was renamed from ``write`` to # ``_write`` to mark it as module-private. ``write`` is # kept as a backward-compatible alias, so the entry-point token in @@ -2512,7 +2512,7 @@ def test_write_geotiff_gpu_rejects_zero_bands(tmp_path): guard must fire there too without dispatching any GPU work.""" import cupy as cp - from xrspatial.geotiff._writers.gpu import write_geotiff_gpu + from xrspatial.geotiff._writers.gpu import _write_geotiff_gpu arr = xr.DataArray( cp.zeros((0, 5, 5), dtype=cp.uint8), @@ -2520,9 +2520,9 @@ def test_write_geotiff_gpu_rejects_zero_bands(tmp_path): ) out = tmp_path / "tmp_2095_zerobands_gpu.tif" with pytest.raises(ValueError) as excinfo: - write_geotiff_gpu(arr, str(out)) + _write_geotiff_gpu(arr, str(out)) msg = str(excinfo.value) - assert msg.startswith("write_geotiff_gpu cannot write") + assert msg.startswith("_write_geotiff_gpu cannot write") assert "0 bands" in msg or "no bands" in msg.lower() assert not out.exists() diff --git a/xrspatial/geotiff/tests/write/test_bigtiff.py b/xrspatial/geotiff/tests/write/test_bigtiff.py index 236fc47bf..41359818c 100644 --- a/xrspatial/geotiff/tests/write/test_bigtiff.py +++ b/xrspatial/geotiff/tests/write/test_bigtiff.py @@ -509,14 +509,14 @@ def test_overhead_matches_actual_emitted_size_via_writer_1905(tmp_path): # ``to_geotiff`` accepts a ``bigtiff`` kwarg but the Parameters block of # the docstring used to jump from ``overview_resampling`` directly to # ``gpu``. -# ``write_geotiff_gpu`` documents the same kwarg correctly, so users +# ``_write_geotiff_gpu`` documents the same kwarg correctly, so users # learning the API from ``to_geotiff(...)`` could not tell the option # existed. This section pins the docstring entry against future drift. import inspect # noqa: E402 import re # noqa: E402 from xrspatial.geotiff import to_geotiff as _to_geotiff_1683 # noqa: E402 -from xrspatial.geotiff import write_geotiff_gpu as _write_geotiff_gpu_1683 # noqa: E402 +from xrspatial.geotiff import _write_geotiff_gpu as _write_geotiff_gpu_1683 # noqa: E402 def _documented_params_1683(fn) -> list[str]: @@ -569,6 +569,6 @@ def test_write_geotiff_gpu_parameters_match_signature_1683(): documented = _documented_params_1683(_write_geotiff_gpu_1683) missing = [p for p in params if p not in documented] assert not missing, ( - f"write_geotiff_gpu docstring is missing parameter " + f"_write_geotiff_gpu docstring is missing parameter " f"descriptions for {missing}; documented params were {documented}" ) diff --git a/xrspatial/geotiff/tests/write/test_cog.py b/xrspatial/geotiff/tests/write/test_cog.py index a398c7f40..1e52b479e 100644 --- a/xrspatial/geotiff/tests/write/test_cog.py +++ b/xrspatial/geotiff/tests/write/test_cog.py @@ -319,9 +319,9 @@ def test_gpu_cog_round_trip(self, tmp_path): gpu_arr = cupy.asarray(arr) path = str(tmp_path / 'cog_1150_gpu_rt.tif') - from xrspatial.geotiff import write_geotiff_gpu - write_geotiff_gpu(gpu_arr, path, crs=4326, compression='deflate', - cog=True, overview_levels=[2]) + from xrspatial.geotiff import _write_geotiff_gpu + _write_geotiff_gpu(gpu_arr, path, crs=4326, compression='deflate', + cog=True, overview_levels=[2]) result = open_geotiff(path) np.testing.assert_array_almost_equal(result.values, arr, decimal=5) @@ -338,9 +338,9 @@ def test_gpu_cog_auto_overviews(self, tmp_path): gpu_arr = cupy.asarray(arr) path = str(tmp_path / 'cog_1150_gpu_auto.tif') - from xrspatial.geotiff import write_geotiff_gpu - write_geotiff_gpu(gpu_arr, path, compression='deflate', - cog=True, tile_size=16) + from xrspatial.geotiff import _write_geotiff_gpu + _write_geotiff_gpu(gpu_arr, path, compression='deflate', + cog=True, tile_size=16) with open(path, 'rb') as f: raw = f.read() @@ -354,10 +354,10 @@ def test_gpu_overview_resampling_nearest(self, tmp_path): gpu_arr = cupy.asarray(arr) path = str(tmp_path / 'cog_1150_gpu_nearest.tif') - from xrspatial.geotiff import write_geotiff_gpu - write_geotiff_gpu(gpu_arr, path, compression='deflate', - cog=True, overview_levels=[2], - overview_resampling='nearest') + from xrspatial.geotiff import _write_geotiff_gpu + _write_geotiff_gpu(gpu_arr, path, compression='deflate', + cog=True, overview_levels=[2], + overview_resampling='nearest') result = open_geotiff(path) np.testing.assert_array_equal(result.values, arr) diff --git a/xrspatial/geotiff/tests/write/test_crs.py b/xrspatial/geotiff/tests/write/test_crs.py index 4bcee76b4..7233c59a9 100644 --- a/xrspatial/geotiff/tests/write/test_crs.py +++ b/xrspatial/geotiff/tests/write/test_crs.py @@ -514,7 +514,7 @@ def test_to_geotiff_numpy_int_crs_writes_real_int_to_attrs_2082(tmp_path): def test_write_geotiff_gpu_numpy_int_crs_roundtrips_2082(tmp_path): import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu arr = cupy.zeros((4, 4), dtype=cupy.float32) da = xr.DataArray( arr, @@ -522,7 +522,7 @@ def test_write_geotiff_gpu_numpy_int_crs_roundtrips_2082(tmp_path): dims=('y', 'x'), ) path = str(tmp_path / "tmp_2082_gpu.tif") - write_geotiff_gpu(da, path, crs=np.int64(4326)) + _write_geotiff_gpu(da, path, crs=np.int64(4326)) out = open_geotiff(path) assert out.attrs.get('crs') == 4326 diff --git a/xrspatial/geotiff/tests/write/test_nodata.py b/xrspatial/geotiff/tests/write/test_nodata.py index ccacaf7ad..24ec08521 100644 --- a/xrspatial/geotiff/tests/write/test_nodata.py +++ b/xrspatial/geotiff/tests/write/test_nodata.py @@ -27,7 +27,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import open_geotiff, read_geotiff_dask, read_vrt, to_geotiff, write_vrt +from xrspatial.geotiff import open_geotiff, _read_geotiff_dask, _read_vrt, to_geotiff, build_vrt from xrspatial.geotiff._attrs import _resolve_nodata_attr from xrspatial.geotiff._geotags import GeoTransform, _parse_nodata_str, build_geo_tags from xrspatial.geotiff._reader import _int_nodata_in_range, _resolve_masked_fill @@ -152,13 +152,13 @@ def test_to_geotiff_vrt_rejects_bool_nodata(tmp_path): def test_write_geotiff_gpu_rejects_bool_nodata(tmp_path): import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu da_cpu = _nan_square() da_gpu = da_cpu.copy(data=cupy.asarray(da_cpu.values)) out = str(tmp_path / "tmp_1973_bool_gpu.tif") with pytest.raises(TypeError, match="nodata must be numeric"): - write_geotiff_gpu(da_gpu, out, nodata=True) + _write_geotiff_gpu(da_gpu, out, nodata=True) # --- All-non-numeric ``attrs['nodatavals']`` warn-and-fall-through ---------- @@ -413,7 +413,7 @@ def test_uint64_max_masked_via_dask(self, tmp_path): da_in = xr.DataArray(arr, dims=("y", "x")) path = os.path.join(str(tmp_path), "t.tif") to_geotiff(da_in, path, nodata=2**64 - 1) - out = read_geotiff_dask(path, chunks=16).compute() + out = _read_geotiff_dask(path, chunks=16).compute() assert out.dtype == np.float64 assert np.isnan(out.values[0, 0]) assert out.values[1, 1] == 100.0 @@ -424,14 +424,14 @@ def test_int64_max_masked_via_dask(self, tmp_path): da_in = xr.DataArray(arr, dims=("y", "x")) path = os.path.join(str(tmp_path), "t.tif") to_geotiff(da_in, path, nodata=2**63 - 1) - out = read_geotiff_dask(path, chunks=16).compute() + out = _read_geotiff_dask(path, chunks=16).compute() assert out.dtype == np.float64 assert np.isnan(out.values[0, 0]) class TestVrtRoundTrip: - """write_vrt -> read_vrt round-trip -- the path that surfaced the bug - in the wild (write_vrt stringifies geo_info.nodata into XML).""" + """write_vrt -> _read_vrt round-trip -- the path that surfaced the bug + in the wild (build_vrt stringifies geo_info.nodata into XML).""" def test_uint64_max_round_trip_via_vrt(self, tmp_path): arr = np.full((16, 16), 100, dtype=np.uint64) @@ -441,7 +441,7 @@ def test_uint64_max_round_trip_via_vrt(self, tmp_path): to_geotiff(da_in, tif_path, nodata=2**64 - 1) vrt_path = os.path.join(str(tmp_path), "t.vrt") - write_vrt(vrt_path, [tif_path]) + build_vrt(vrt_path, [tif_path]) # The VRT XML should carry the integer string literal, not a # scientific-notation float that loses one ULP at the dtype max. @@ -449,7 +449,7 @@ def test_uint64_max_round_trip_via_vrt(self, tmp_path): xml = f.read() assert "18446744073709551615" in xml - out = read_vrt(vrt_path) + out = _read_vrt(vrt_path) assert out.dtype == np.float64 assert np.isnan(out.values[0, 0]) assert out.values[1, 1] == 100.0 @@ -463,13 +463,13 @@ def test_int64_max_round_trip_via_vrt(self, tmp_path): to_geotiff(da_in, tif_path, nodata=2**63 - 1) vrt_path = os.path.join(str(tmp_path), "t.vrt") - write_vrt(vrt_path, [tif_path]) + build_vrt(vrt_path, [tif_path]) with open(vrt_path) as f: xml = f.read() assert "9223372036854775807" in xml - out = read_vrt(vrt_path) + out = _read_vrt(vrt_path) assert out.dtype == np.float64 assert np.isnan(out.values[0, 0]) assert out.values[1, 1] == 100.0 @@ -478,7 +478,7 @@ def test_int64_max_round_trip_via_vrt(self, tmp_path): class TestGpuPathParity: @requires_gpu def test_uint64_max_masked_via_gpu(self, tmp_path): - from xrspatial.geotiff import read_geotiff_gpu + from xrspatial.geotiff import _read_geotiff_gpu arr = np.full((16, 16), 100, dtype=np.uint64) arr[0, 0] = 2**64 - 1 @@ -486,7 +486,7 @@ def test_uint64_max_masked_via_gpu(self, tmp_path): path = os.path.join(str(tmp_path), "t.tif") to_geotiff(da_in, path, nodata=2**64 - 1) - gpu_da = read_geotiff_gpu(path) + gpu_da = _read_geotiff_gpu(path) host = gpu_da.data.get() assert host.dtype == np.float64 assert np.isnan(host[0, 0]) @@ -555,7 +555,7 @@ def test_read_geotiff_dask_uint16_negative_nodata_graph( uint16_neg_nodata_tif): """The dask graph-construction path no longer crashes.""" path, _ = uint16_neg_nodata_tif - result = read_geotiff_dask(path, chunks=2) + result = _read_geotiff_dask(path, chunks=2) # No promotion to float64 -- sentinel is unrepresentable so masking # would be a no-op anyway. assert result.dtype == np.uint16 @@ -567,7 +567,7 @@ def test_read_geotiff_dask_uint16_negative_nodata_compute( uint16_neg_nodata_tif): """Dask compute returns the file's pixels unchanged.""" path, expected = uint16_neg_nodata_tif - result = read_geotiff_dask(path, chunks=2).compute() + result = _read_geotiff_dask(path, chunks=2).compute() assert result.dtype == np.uint16 np.testing.assert_array_equal(result.values, expected) @@ -735,7 +735,7 @@ def test_dtype_cast_preservation_uint8(tmp_path): def test_dask_path_mask_nodata_false(uint16_with_matching_sentinel): """The dask path honours the kwarg too: integer source dtype survives. - Without this, ``read_geotiff_dask`` would still promote the dask + Without this, ``_read_geotiff_dask`` would still promote the dask graph dtype to float64 and force the per-chunk cast. """ path, arr = uint16_with_matching_sentinel diff --git a/xrspatial/geotiff/tests/write/test_overview.py b/xrspatial/geotiff/tests/write/test_overview.py index 878bfef17..b6fade1e3 100644 --- a/xrspatial/geotiff/tests/write/test_overview.py +++ b/xrspatial/geotiff/tests/write/test_overview.py @@ -2504,21 +2504,21 @@ def test_block_reduce_2d_gpu_matches_cpu_with_nan(method): ('median', _RAMP_EXPECTED_MEDIAN), ]) def test_write_geotiff_gpu_cog_overview_resampling(tmp_path, method, expected): - """End-to-end: ``write_geotiff_gpu(cog=True, overview_resampling=method)`` + """End-to-end: ``_write_geotiff_gpu(cog=True, overview_resampling=method)`` writes a COG whose overview level 1 matches the closed-form 2x2 reduction. Exercises the GPU make-overview path including the dispatch on ``method``.""" import cupy - from xrspatial.geotiff import write_geotiff_gpu + from xrspatial.geotiff import _write_geotiff_gpu arr = _arr_4x4_ramp() arr_gpu = cupy.asarray(arr) da = xr.DataArray(arr_gpu, dims=['y', 'x']) p = str(tmp_path / f'cog_{method}_gpu.tif') - write_geotiff_gpu(da, p, cog=True, compression='deflate', tiled=True, - tile_size=16, overview_levels=[2], - overview_resampling=method) + _write_geotiff_gpu(da, p, cog=True, compression='deflate', tiled=True, + tile_size=16, overview_levels=[2], + overview_resampling=method) ov = open_geotiff(p, overview_level=1) np.testing.assert_allclose(np.asarray(ov.data), expected)