From 0d21c42f798c9b418bca4335a76964b25c0a19f4 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 5 Jun 2026 12:14:08 -0700 Subject: [PATCH 1/4] Privatize build_vrt as _build_vrt; route to_geotiff's VRT path through it (#2974) build_vrt is dropped from the public API (__all__, package docstring) and renamed to _build_vrt. to_geotiff's .vrt write path (_write_vrt_tiled) now routes through _build_vrt instead of calling _vrt.write_vrt directly, so _build_vrt is the single internal VRT-index entry point. Tests updated to import the private name; the public-surface assertions now expect only open_geotiff and to_geotiff. --- xrspatial/geotiff/__init__.py | 31 +++--- xrspatial/geotiff/_backends/vrt.py | 4 +- xrspatial/geotiff/_crs.py | 8 +- xrspatial/geotiff/_runtime.py | 8 +- xrspatial/geotiff/_validation.py | 2 +- xrspatial/geotiff/_vrt.py | 4 +- xrspatial/geotiff/_writer.py | 4 +- xrspatial/geotiff/_writers/__init__.py | 2 +- xrspatial/geotiff/_writers/eager.py | 16 ++- xrspatial/geotiff/_writers/gpu.py | 2 +- xrspatial/geotiff/_writers/vrt.py | 77 +++++++------ .../geotiff/tests/golden_corpus/test_vrt.py | 8 +- .../tests/gpu/test_kernels_and_kwargs.py | 8 +- .../tests/integration/test_http_sources.py | 6 +- .../tests/parity/test_api_consolidation.py | 32 ++++-- .../tests/parity/test_backend_matrix.py | 6 +- .../geotiff/tests/parity/test_finalization.py | 30 ++--- .../tests/parity/test_pixel_equality.py | 4 +- .../tests/parity/test_signature_contract.py | 14 +-- xrspatial/geotiff/tests/read/test_basic.py | 2 +- .../tests/read/test_rioxarray_compat_2961.py | 6 +- .../tests/release_gates/test_features.py | 16 +-- xrspatial/geotiff/tests/test_polish.py | 18 +-- xrspatial/geotiff/tests/test_round_trip.py | 6 +- .../geotiff/tests/unit/test_signatures.py | 52 ++++----- xrspatial/geotiff/tests/vrt/test_metadata.py | 36 +++--- .../geotiff/tests/vrt/test_validation.py | 4 +- xrspatial/geotiff/tests/write/test_basic.py | 104 +++++++++--------- xrspatial/geotiff/tests/write/test_nodata.py | 8 +- 29 files changed, 263 insertions(+), 255 deletions(-) diff --git a/xrspatial/geotiff/__init__.py b/xrspatial/geotiff/__init__.py index addf106f..3f643b52 100644 --- a/xrspatial/geotiff/__init__.py +++ b/xrspatial/geotiff/__init__.py @@ -15,12 +15,10 @@ 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``. + +VRT mosaics are written by passing a ``.vrt`` path to ``to_geotiff``; the +underlying index emitter (``_build_vrt``) is internal and not part of the +public surface. The backend functions ``_read_geotiff_gpu``, ``_read_geotiff_dask``, ``_read_vrt``, and ``_write_geotiff_gpu`` are private. ``open_geotiff`` and @@ -95,7 +93,10 @@ # 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 +# Re-export only: the internal VRT-index emitter. ``to_geotiff``'s ``.vrt`` +# path reaches it via ``_writers.eager``; bound here so tests and internal +# callers can import ``xrspatial.geotiff._build_vrt``. Not public API. +from ._writers.vrt import _build_vrt # noqa: F401 # All names below are part of the supported public API. ``plot_geotiff`` # is intentionally omitted: it is deprecated in favour of ``da.xrs.plot()`` @@ -126,7 +127,6 @@ 'UnsafeURLError', 'UnsupportedGeoTIFFFeatureError', 'VRTStableSourcesOnlyError', - 'build_vrt', 'open_geotiff', 'to_geotiff', ] @@ -847,15 +847,12 @@ def open_geotiff(source: str | BinaryIO, *, Examples -------- - Safe VRT usage. Mosaic two compatible tiles and read with the - fail-closed defaults: - - >>> 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 = open_geotiff(vrt_path) # doctest: +SKIP + Safe VRT usage. Write a ``.vrt`` mosaic with ``to_geotiff`` and read + it back with the fail-closed defaults: + + >>> from xrspatial.geotiff import open_geotiff, to_geotiff + >>> to_geotiff(data, 'mosaic.vrt') # doctest: +SKIP + >>> da = open_geotiff('mosaic.vrt') # doctest: +SKIP Intentionally raises. A VRT whose source tiles disagree on their per-band nodata sentinels is rejected by the default diff --git a/xrspatial/geotiff/_backends/vrt.py b/xrspatial/geotiff/_backends/vrt.py index 023ce348..704e4eb6 100644 --- a/xrspatial/geotiff/_backends/vrt.py +++ b/xrspatial/geotiff/_backends/vrt.py @@ -341,8 +341,8 @@ def _read_vrt(source: str, *, Safe usage. Mosaic two compatible tiles and read with the fail-closed defaults: - >>> from xrspatial.geotiff import open_geotiff, build_vrt - >>> vrt_path = build_vrt( # doctest: +SKIP + >>> from xrspatial.geotiff import _build_vrt + >>> vrt_path = _build_vrt( # doctest: +SKIP ... 'mosaic.vrt', ... source_files=['tile_west.tif', 'tile_east.tif'], ... ) diff --git a/xrspatial/geotiff/_crs.py b/xrspatial/geotiff/_crs.py index db912e1b..0415c349 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 ``build_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 @@ -231,10 +231,10 @@ def _resolve_crs_to_wkt(crs) -> str | None: 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.build_vrt``, which only + ``crs`` is ``None``) for forwarding to ``_vrt.write_vrt``, which only speaks WKT. - Used by ``build_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.build_vrt semantics (it + # GeoTIFFs; preserve the existing _vrt.write_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/_runtime.py b/xrspatial/geotiff/_runtime.py index 09744b03..11d8303d 100644 --- a/xrspatial/geotiff/_runtime.py +++ b/xrspatial/geotiff/_runtime.py @@ -24,7 +24,7 @@ # on_gpu_failure=" (forward verbatim). _GPU_DEPRECATED_SENTINEL = object() _ON_GPU_FAILURE_SENTINEL = object() -# ``build_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=. @@ -34,17 +34,17 @@ # kwarg up front for non-VRT sources) from "caller set missing_sources=" # (forward verbatim to _read_vrt). Mirrors the on_gpu_failure pattern. _MISSING_SOURCES_SENTINEL = object() -# ``build_vrt`` historically named its first positional kwarg ``vrt_path`` +# ``_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() -# ``build_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, ``build_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 0acfbded..d5b5e7b9 100644 --- a/xrspatial/geotiff/_validation.py +++ b/xrspatial/geotiff/_validation.py @@ -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`` / ``build_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: diff --git a/xrspatial/geotiff/_vrt.py b/xrspatial/geotiff/_vrt.py index 32ea63da..c3c9ee16 100644 --- a/xrspatial/geotiff/_vrt.py +++ b/xrspatial/geotiff/_vrt.py @@ -1893,7 +1893,7 @@ def _check_no_mixed_georef(sources_meta: list[dict]) -> None: Refuse rather than emit a mosaic that mislocates the tile. The all-georeferenced and all-non-georeferenced cases both pass: - build_vrt emits a ```` only when every source is + write_vrt emits a ```` only when every source is georeferenced, so the all-non-georeferenced VRT preserves ``georef_status='none'`` on read. """ @@ -2085,7 +2085,7 @@ def write_vrt(vrt_path: str, source_files: list[str], *, # Enforce the docstring contract: every source must agree with the # first on pixel size, sample format + bits-per-sample (i.e. dtype), - # band count, and CRS WKT. Without this, build_vrt would silently + # band count, and CRS WKT. Without this, write_vrt would silently # produce a syntactically valid VRT that misplaces or mis-types data # downstream. # diff --git a/xrspatial/geotiff/_writer.py b/xrspatial/geotiff/_writer.py index 33becca0..3fef0a04 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.build_vrt`. Direct callers of the helpers +:func:`xrspatial.geotiff._write_geotiff_gpu`, and the internal +: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 d1c6abf9..d75d7f51 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 ``build_vrt`` in sibling modules. The package ``__init__`` stays +and the internal ``_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 939f2f5e..2ea8abf4 100644 --- a/xrspatial/geotiff/_writers/eager.py +++ b/xrspatial/geotiff/_writers/eager.py @@ -4,9 +4,9 @@ ``_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 ``build_vrt``; -``to_geotiff`` dispatches to them when the caller asks for a GPU -output or a ``.vrt`` path. +``_writers/vrt.py`` hold the GPU writer and the internal ``_build_vrt`` +VRT-index emitter; ``to_geotiff`` dispatches to them when the caller +asks for a GPU output or a ``.vrt`` path. """ from __future__ import annotations @@ -341,7 +341,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 ``build_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. @@ -1460,9 +1460,13 @@ def _safe_write_tile(*args, **kwargs): # Write VRT index with relative paths. The VRT lives at ``vrt_path``; # tile paths now resolve under the final ``tiles_dir``. tile_paths = [os.path.join(tiles_dir, name) for name in tile_names] - from .._vrt import write_vrt as _write_vrt_fn + # Route through the internal ``_build_vrt`` helper so it is the single + # entry point for VRT-index emission (it wraps ``_vrt.write_vrt`` with + # the shared nodata/crs normalisation). ``nodata`` was already resolved + # and validated above; passing it again is idempotent. + from .vrt import _build_vrt try: - _write_vrt_fn(vrt_path, tile_paths, relative=True, nodata=nodata) + _build_vrt(vrt_path, tile_paths, relative=True, nodata=nodata) except BaseException: # The index step failed after the rename. Remove the now-renamed # tile dir too so a retry is not blocked by the leftover-state diff --git a/xrspatial/geotiff/_writers/gpu.py b/xrspatial/geotiff/_writers/gpu.py index 345ba4f0..5eb5582c 100644 --- a/xrspatial/geotiff/_writers/gpu.py +++ b/xrspatial/geotiff/_writers/gpu.py @@ -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 ``build_vrt`` so callers can handle + mirrors ``to_geotiff`` and ``_build_vrt`` so callers can handle the three writers uniformly. Raises diff --git a/xrspatial/geotiff/_writers/vrt.py b/xrspatial/geotiff/_writers/vrt.py index cd8128d7..fa6ba143 100644 --- a/xrspatial/geotiff/_writers/vrt.py +++ b/xrspatial/geotiff/_writers/vrt.py @@ -1,9 +1,10 @@ -"""VRT writer entry point. +"""Internal VRT writer. -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``. +Wraps ``_vrt.write_vrt`` with the ``_build_vrt`` helper: 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``. ``to_geotiff``'s +``.vrt`` write path is the only caller; it is not part of the public API. """ from __future__ import annotations @@ -15,26 +16,22 @@ from .._validation import _validate_nodata_arg -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, - crs: int | str | None = None, - crs_wkt: str | None = _CRS_WKT_DEPRECATED_SENTINEL, - nodata: float | int | None = None) -> str: +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, + crs: int | str | None = None, + crs_wkt: str | None = _CRS_WKT_DEPRECATED_SENTINEL, + nodata: float | int | None = None) -> str: """Generate a VRT file that mosaics multiple GeoTIFF tiles. - Release-contract tier (see - ``docs/source/reference/release_gate_geotiff.rst`` and - ``docs/source/reference/geotiff_release_contract.rst``): the - entry point is [advanced]. VRT mosaic output is supported but - targets a narrow subset of GDAL's VRT spec; the caller should - know the failure modes on the read side. A consumer reading the - resulting ``.vrt`` may hit cross-source nodata mismatch, missing - backing files, or per-band metadata disagreement. Full GDAL VRT - parity, warped / reprojection VRTs, and nested VRTs are out of - scope for this release. See - :data:`xrspatial.geotiff.SUPPORTED_FEATURES` for the full tier map. + [internal-only] This helper backs ``to_geotiff``'s ``.vrt`` write + path and is not part of the public API. VRT mosaic output targets a + narrow subset of GDAL's VRT spec; a consumer reading the resulting + ``.vrt`` may hit cross-source nodata mismatch, missing backing files, + or per-band metadata disagreement. Full GDAL VRT parity, warped / + reprojection VRTs, and nested VRTs are out of scope for this release. + See :data:`xrspatial.geotiff.SUPPORTED_FEATURES` for the full tier map. Output targets the same narrow subset of GDAL's VRT spec that the reader supports (see the "VRT support matrix" section in @@ -67,8 +64,8 @@ def build_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 (``build_vrt(vrt_path, sources)`` positional or - ``build_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 @@ -108,8 +105,8 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, below are illustrative; replace with paths to real GeoTIFF files on disk: - >>> from xrspatial.geotiff import build_vrt, open_geotiff - >>> vrt_path = build_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'], ... ) @@ -124,7 +121,7 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, >>> from xrspatial.geotiff import MixedBandMetadataError >>> # tile_a.tif declares nodata=-9999; tile_b.tif declares nodata=0 - >>> bad_path = build_vrt( # doctest: +SKIP + >>> bad_path = _build_vrt( # doctest: +SKIP ... 'mixed_nodata.vrt', ... source_files=['tile_a.tif', 'tile_b.tif'], ... ) @@ -135,7 +132,7 @@ def build_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.build_vrt`` for the + # without parsing the docstring. Mirrors ``_vrt.write_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. @@ -145,8 +142,8 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # uniformly against a single ``vrt_path`` local. ``path`` is the # 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 ``build_vrt(vrt_path, sources)`` or - # keyword ``build_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,11 +152,11 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # picking one. Mirrors the same rule the ``crs`` / ``crs_wkt`` # shim below applies. raise TypeError( - "build_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( - "build_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 " "destination kwarg.", @@ -172,19 +169,19 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # required positional argument`` semantics by raising rather than # forwarding the sentinel into ``_write_vrt_internal``. raise TypeError( - "build_vrt: missing required argument 'path'") + "_build_vrt: missing required argument 'path'") if path is None: - # Explicit ``path=None`` (including positional ``build_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( - "build_vrt: 'path' must be a str, got None") + "_build_vrt: 'path' must be a str, got None") if source_files is None: raise TypeError( - "build_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,11 +189,11 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, # to encode the same CRS as the int. Refuse rather than silently # picking one. raise TypeError( - "build_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( - "build_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 " "an int EPSG code or a WKT string.", @@ -205,7 +202,7 @@ def build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, ) crs = crs_wkt - # Reject bool / non-numeric nodata at the entry point so build_vrt + # 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))`` diff --git a/xrspatial/geotiff/tests/golden_corpus/test_vrt.py b/xrspatial/geotiff/tests/golden_corpus/test_vrt.py index e2671be8..70f56056 100644 --- a/xrspatial/geotiff/tests/golden_corpus/test_vrt.py +++ b/xrspatial/geotiff/tests/golden_corpus/test_vrt.py @@ -8,14 +8,14 @@ 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 ``build_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 ``build_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 build_vrt, open_geotiff # noqa: E402 +from xrspatial.geotiff import _build_vrt, open_geotiff # 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" - build_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_kernels_and_kwargs.py b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py index 73c97066..ee26d381 100644 --- a/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py +++ b/xrspatial/geotiff/tests/gpu/test_kernels_and_kwargs.py @@ -2074,11 +2074,11 @@ 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 build_vrt + from xrspatial.geotiff import _build_vrt tif = _make_tif_1776(tmp_path) vrt = os.path.join(str(tmp_path), 'src_1776.vrt') - build_vrt(vrt, [tif]) + _build_vrt(vrt, [tif]) return vrt @@ -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 build_vrt + from xrspatial.geotiff import _build_vrt from xrspatial.geotiff._writer import write arr = np.array( @@ -2440,7 +2440,7 @@ 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") - build_vrt(vrt_path, [tif_path]) + _build_vrt(vrt_path, [tif_path]) return vrt_path, arr diff --git a/xrspatial/geotiff/tests/integration/test_http_sources.py b/xrspatial/geotiff/tests/integration/test_http_sources.py index 3c85fd6f..182e32fd 100644 --- a/xrspatial/geotiff/tests/integration/test_http_sources.py +++ b/xrspatial/geotiff/tests/integration/test_http_sources.py @@ -24,7 +24,7 @@ from xrspatial.geotiff import UnsafeURLError, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt from xrspatial.geotiff import _reader as _reader_mod from xrspatial.geotiff import _sources as _sources_mod -from xrspatial.geotiff import _write_geotiff_gpu, build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _write_geotiff_gpu, _build_vrt, open_geotiff, to_geotiff 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, @@ -6042,7 +6042,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') - build_vrt(vrt, [src]) + _build_vrt(vrt, [src]) return vrt, src @@ -6228,7 +6228,7 @@ def test_explicit_none_max_cloud_bytes_rejected_on_vrt_path_2026_05_15( _read_vrt, to_geotiff, _write_geotiff_gpu, - build_vrt, + _build_vrt, ) diff --git a/xrspatial/geotiff/tests/parity/test_api_consolidation.py b/xrspatial/geotiff/tests/parity/test_api_consolidation.py index 686f1a58..f296494d 100644 --- a/xrspatial/geotiff/tests/parity/test_api_consolidation.py +++ b/xrspatial/geotiff/tests/parity/test_api_consolidation.py @@ -1,11 +1,12 @@ """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. +The geotiff read/write surface is ``open_geotiff`` / ``to_geotiff``. 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. The VRT-index +emitter ``_build_vrt`` is also private: ``to_geotiff``'s ``.vrt`` path +reaches it, and it has no DataArray to write (it indexes files that +already exist), so it is not part of the public surface (issue #2974). These tests pin the new surface and confirm the dispatchers still reach each backend. @@ -14,7 +15,7 @@ import pytest import xrspatial.geotiff as g -from xrspatial.geotiff import build_vrt, open_geotiff, to_geotiff +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 @@ -34,9 +35,18 @@ def test_public_read_write_surface_is_consolidated(): - """The only lowercase ``__all__`` entries are the three public funcs.""" + """The only lowercase ``__all__`` entries are the two public funcs.""" fns = {name for name in g.__all__ if name[0].islower()} - assert fns == {"open_geotiff", "to_geotiff", "build_vrt"} + assert fns == {"open_geotiff", "to_geotiff"} + + +def test_build_vrt_is_not_public(): + """``_build_vrt`` is internal: not in ``__all__`` and no public alias.""" + assert "build_vrt" not in g.__all__ + assert "_build_vrt" not in g.__all__ + assert not hasattr(g, "build_vrt") + # Still importable under its private name for internal callers / tests. + assert callable(g._build_vrt) @pytest.mark.parametrize("name", _OLD_PUBLIC_NAMES) @@ -66,12 +76,12 @@ def _two_tile_vrt(tmp_path): 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]) + _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.""" + """_build_vrt is the internal mosaic builder; open_geotiff reads it back.""" vrt_path = _two_tile_vrt(tmp_path) mosaic = open_geotiff(vrt_path) assert mosaic.shape == (4, 8) diff --git a/xrspatial/geotiff/tests/parity/test_backend_matrix.py b/xrspatial/geotiff/tests/parity/test_backend_matrix.py index affcf10e..7b942377 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 _read_vrt, build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _read_vrt, _build_vrt, open_geotiff, to_geotiff 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)) - build_vrt(str(target), tile_paths, relative=False, crs=4326) + _build_vrt(str(target), tile_paths, relative=False, crs=4326) return target @@ -1179,7 +1179,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(): - build_vrt(str(vrt_path), [str(local_src)]) + _build_vrt(str(vrt_path), [str(local_src)]) return open_geotiff(str(vrt_path), **_FP_OPTIN) diff --git a/xrspatial/geotiff/tests/parity/test_finalization.py b/xrspatial/geotiff/tests/parity/test_finalization.py index 7ac33181..2b210c91 100644 --- a/xrspatial/geotiff/tests/parity/test_finalization.py +++ b/xrspatial/geotiff/tests/parity/test_finalization.py @@ -35,7 +35,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, build_vrt, +from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _build_vrt, open_geotiff, to_geotiff) from xrspatial.geotiff._attrs import (GEOREF_STATUS_CRS_ONLY, GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_ROTATED_DROPPED, @@ -73,11 +73,11 @@ def _build_local_tif(tmp_path, name='src_2175.tif'): return path -def _build_vrt(tmp_path): +def _make_one_source_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') - build_vrt(vrt, [src]) + _build_vrt(vrt, [src]) return vrt, src @@ -143,19 +143,19 @@ def test_gpu_overview_level_float(tmp_path): @pytest.mark.parametrize("value", [True, False]) def test_vrt_overview_level_bool(tmp_path, value): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(TypeError, match="bool"): _read_vrt(vrt, overview_level=value) def test_vrt_overview_level_str(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(TypeError, match="str"): _read_vrt(vrt, overview_level="0") def test_vrt_overview_level_float(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(TypeError, match="float"): _read_vrt(vrt, overview_level=1.0) @@ -176,7 +176,7 @@ def test_open_geotiff_gpu_rejects_max_cloud_bytes(tmp_path): def test_open_geotiff_vrt_rejects_max_cloud_bytes(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): open_geotiff(vrt, max_cloud_bytes=8) @@ -194,7 +194,7 @@ def test_gpu_rejects_max_cloud_bytes(tmp_path): def test_vrt_rejects_max_cloud_bytes(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): _read_vrt(vrt, max_cloud_bytes=8) @@ -217,7 +217,7 @@ def test_explicit_none_max_cloud_bytes_rejected_on_gpu_direct(tmp_path): def test_explicit_none_max_cloud_bytes_rejected_on_vrt_direct(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(ValueError, match=r"max_cloud_bytes"): _read_vrt(vrt, max_cloud_bytes=None) @@ -280,7 +280,7 @@ def test_dask_rejects_on_gpu_failure(tmp_path): def test_vrt_rejects_on_gpu_failure(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) with pytest.raises(ValueError, match=r"on_gpu_failure only applies"): _read_vrt(vrt, on_gpu_failure='strict') @@ -347,7 +347,7 @@ def test_dask_accepts_path_object(tmp_path): def test_vrt_accepts_path_object(tmp_path): from pathlib import Path - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) out = _read_vrt(Path(vrt)) assert out.shape == (8, 8) @@ -402,7 +402,7 @@ def test_dask_defaults_round_trip(tmp_path): def test_vrt_defaults_round_trip(tmp_path): - vrt, _src = _build_vrt(tmp_path) + vrt, _src = _make_one_source_vrt(tmp_path) out = _read_vrt(vrt) assert out.shape == (8, 8) @@ -425,7 +425,7 @@ def _get_error(callable_, *args, **kwargs): def test_max_cloud_bytes_message_parity(tmp_path): path = _build_local_tif(tmp_path) - vrt, _ = _build_vrt(tmp_path) + vrt, _ = _make_one_source_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) # Both raise ValueError with the same dask-incompatibility message. @@ -478,7 +478,7 @@ def test_missing_sources_message_parity(tmp_path): def test_on_gpu_failure_message_parity(tmp_path): path = _build_local_tif(tmp_path) - vrt, _ = _build_vrt(tmp_path) + vrt, _ = _make_one_source_vrt(tmp_path) results = [ _get_error(open_geotiff, path, on_gpu_failure='strict'), _get_error(_read_geotiff_dask, path, on_gpu_failure='strict'), @@ -491,7 +491,7 @@ def test_on_gpu_failure_message_parity(tmp_path): def test_overview_level_message_parity(tmp_path): path = _build_local_tif(tmp_path) - vrt, _ = _build_vrt(tmp_path) + vrt, _ = _make_one_source_vrt(tmp_path) results = [ _get_error(open_geotiff, path, overview_level="bad"), _get_error(_read_geotiff_dask, path, overview_level="bad"), diff --git a/xrspatial/geotiff/tests/parity/test_pixel_equality.py b/xrspatial/geotiff/tests/parity/test_pixel_equality.py index 33d7f39b..787bf259 100644 --- a/xrspatial/geotiff/tests/parity/test_pixel_equality.py +++ b/xrspatial/geotiff/tests/parity/test_pixel_equality.py @@ -30,7 +30,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, build_vrt, +from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _build_vrt, open_geotiff, to_geotiff) 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" - build_vrt(str(vrt_path), tile_paths, relative=False, crs=4326) + _build_vrt(str(vrt_path), tile_paths, relative=False, crs=4326) return vrt_path diff --git a/xrspatial/geotiff/tests/parity/test_signature_contract.py b/xrspatial/geotiff/tests/parity/test_signature_contract.py index b8cacace..67bd2071 100644 --- a/xrspatial/geotiff/tests/parity/test_signature_contract.py +++ b/xrspatial/geotiff/tests/parity/test_signature_contract.py @@ -6,7 +6,7 @@ Three sections, each a former top-level file: Section 1 -- Writer signature / docstring parity - ``build_vrt`` exposes its documented kwargs through an explicit + ``_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``. @@ -36,7 +36,7 @@ import xarray as xr from xrspatial.geotiff import (SUPPORTED_FEATURES, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, - _write_geotiff_gpu, build_vrt, open_geotiff, to_geotiff) + _write_geotiff_gpu, _build_vrt, open_geotiff, to_geotiff) from .._helpers.markers import requires_gpu @@ -45,7 +45,7 @@ # =========================================================================== # # Three drifts this section guards against: -# ``build_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 @@ -53,7 +53,7 @@ def test_write_vrt_signature_exposes_documented_kwargs(): - """``inspect.signature(build_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 @@ -62,7 +62,7 @@ def test_write_vrt_signature_exposes_documented_kwargs(): deprecation shim can tell "user passed nothing" from "user passed crs_wkt=None"). """ - sig = inspect.signature(build_vrt) + sig = inspect.signature(_build_vrt) params = sig.parameters assert 'relative' in params assert 'crs' in params # canonical kwarg @@ -99,7 +99,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'): - build_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 +118,7 @@ def test_write_vrt_accepts_documented_kwargs(tmp_path): to_geotiff(da, tif_path) vrt_path = str(tmp_path / 't.vrt') - out = build_vrt( + out = _build_vrt( vrt_path, [tif_path], relative=False, crs=None, nodata=-9999.0, ) diff --git a/xrspatial/geotiff/tests/read/test_basic.py b/xrspatial/geotiff/tests/read/test_basic.py index 60e72ab3..5155ff3c 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 -from xrspatial.geotiff import build_vrt as _write_vrt_1810 +from xrspatial.geotiff import _build_vrt as _write_vrt_1810 from xrspatial.geotiff import open_geotiff, to_geotiff 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 diff --git a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py index 3b9ce585..b2551ac0 100644 --- a/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py +++ b/xrspatial/geotiff/tests/read/test_rioxarray_compat_2961.py @@ -14,7 +14,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _build_vrt, open_geotiff, to_geotiff from xrspatial.geotiff._runtime import GeoTIFFFallbackWarning @@ -226,13 +226,13 @@ def test_parse_coordinates_false_gpu_rejected(tmp_path): def test_mask_and_scale_vrt_rejected(tmp_path): src = _int_sentinel_tiff(str(tmp_path / "t2961_gate_vrt_src.tif")) - vrt = build_vrt(str(tmp_path / "t2961_gate.vrt"), source_files=[src]) + vrt = _build_vrt(str(tmp_path / "t2961_gate.vrt"), source_files=[src]) with pytest.raises(ValueError, match="mask_and_scale.*.vrt"): open_geotiff(vrt, mask_and_scale=True) def test_parse_coordinates_false_vrt_rejected(tmp_path): src = _int_sentinel_tiff(str(tmp_path / "t2961_gate_vrt_pc_src.tif")) - vrt = build_vrt(str(tmp_path / "t2961_gate_pc.vrt"), source_files=[src]) + vrt = _build_vrt(str(tmp_path / "t2961_gate_pc.vrt"), source_files=[src]) with pytest.raises(ValueError, match="parse_coordinates=False.*.vrt"): open_geotiff(vrt, parse_coordinates=False) diff --git a/xrspatial/geotiff/tests/release_gates/test_features.py b/xrspatial/geotiff/tests/release_gates/test_features.py index 6262324b..d0e0ae94 100644 --- a/xrspatial/geotiff/tests/release_gates/test_features.py +++ b/xrspatial/geotiff/tests/release_gates/test_features.py @@ -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): - """build_vrt generates a valid VRT that reads back correctly.""" - from xrspatial.geotiff import build_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') - build_vrt(vrt_path, [lpath, rpath]) + _build_vrt(vrt_path, [lpath, rpath]) da = open_geotiff(vrt_path) assert da.shape == (4, 8) @@ -2871,11 +2871,11 @@ def test_all_lists_supported_functions(self): # ``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 / to_geotiff). The backend functions + # (_read_geotiff_gpu, _read_geotiff_dask, _read_vrt, + # _write_geotiff_gpu) and the VRT-index emitter (_build_vrt) + # are private; the dispatchers route to them from their kwargs + # and the ``.vrt`` output path (issue #2974). 'open_geotiff', 'to_geotiff', } diff --git a/xrspatial/geotiff/tests/test_polish.py b/xrspatial/geotiff/tests/test_polish.py index e356aef2..cfea4f8a 100644 --- a/xrspatial/geotiff/tests/test_polish.py +++ b/xrspatial/geotiff/tests/test_polish.py @@ -5,7 +5,7 @@ * early ``compression`` validation in ``to_geotiff`` * read dispatch leaves ``_read_geotiff_dask`` with a defensive ``.vrt`` fallback that delegates to ``_read_vrt`` -* ``build_vrt`` docstring lists kwargs (rejects unknown ones) +* ``_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, build_vrt, to_geotiff +from xrspatial.geotiff import _read_geotiff_dask, _build_vrt, to_geotiff from xrspatial.geotiff._reader import _MmapCache, read_to_array from xrspatial.geotiff._writer import _MAX_OVERVIEW_LEVELS, write @@ -63,7 +63,7 @@ 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 # VRT reader without trying to parse XML as TIFF. - from xrspatial.geotiff import build_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') @@ -80,7 +80,7 @@ def test_read_geotiff_dask_handles_vrt_directly(self, tmp_path): # --------------------------------------------------------------------------- -# build_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. - build_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): - build_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 build_vrt.__doc__ - assert 'crs_wkt' in build_vrt.__doc__ - assert 'nodata' in build_vrt.__doc__ + assert 'relative' in _build_vrt.__doc__ + assert 'crs_wkt' in _build_vrt.__doc__ + assert 'nodata' in _build_vrt.__doc__ # --------------------------------------------------------------------------- diff --git a/xrspatial/geotiff/tests/test_round_trip.py b/xrspatial/geotiff/tests/test_round_trip.py index fe2bc95d..56dfda5e 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 build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _build_vrt, open_geotiff, to_geotiff 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 ``build_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" - build_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/unit/test_signatures.py b/xrspatial/geotiff/tests/unit/test_signatures.py index ea4dec0e..f6b65892 100644 --- a/xrspatial/geotiff/tests/unit/test_signatures.py +++ b/xrspatial/geotiff/tests/unit/test_signatures.py @@ -37,7 +37,7 @@ 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``, ``build_vrt`` + / ``_read_geotiff_dask`` ``name`` and ``max_pixels``, ``_build_vrt`` ``relative`` / ``crs`` / ``nodata``, GPU reader ``dtype``, GPU writer ``bigtiff`` / ``predictor``, and ``_read_vrt`` ``window``. @@ -64,7 +64,7 @@ import xrspatial.geotiff._compression as comp_mod from xrspatial.geotiff import (GeoTIFFFallbackWarning, _geotiff_strict_mode, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _wkt_to_epsg, _write_geotiff_gpu, - build_vrt, open_geotiff, to_geotiff) + _build_vrt, open_geotiff, to_geotiff) 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, @@ -143,15 +143,15 @@ def test_write_geotiff_gpu_path_annotated(): def test_write_vrt_path_annotated(): - """``build_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``). The annotation is plain ``str``: the default value is a private sentinel (not ``None``) so the deprecation shim can distinguish - ``build_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(build_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(build_vrt, 'vrt_path') == 'str | None' + assert _annotation(_build_vrt, 'vrt_path') == 'str | None' # --- source: str or BinaryIO (open_geotiff is the public dispatch) --- @@ -248,7 +248,7 @@ def test_write_geotiff_gpu_nodata_annotated(): def test_write_vrt_nodata_annotated(): """Pre-existing annotation -- keep it pinned.""" - assert _annotation(build_vrt, 'nodata') == 'float | int | None' + assert _annotation(_build_vrt, 'nodata') == 'float | int | None' # --- streaming_buffer_bytes: int on both writer entry points --- @@ -1764,7 +1764,7 @@ 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 -- ``build_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 @@ -1832,7 +1832,7 @@ def small_tiff_path(tmp_path): return str(p), arr -# --- 6a: build_vrt override effect (relative / crs / nodata) + error path --- +# --- 6a: _build_vrt override effect (relative / crs / nodata) + error path --- class TestWriteVrtRelativeBehaviour: @@ -1846,7 +1846,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') - build_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, @@ -1863,7 +1863,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') - build_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 @@ -1880,7 +1880,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') - build_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 @@ -1893,7 +1893,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') - build_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 ( @@ -1927,7 +1927,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') - build_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 @@ -1937,7 +1937,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') - build_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) @@ -1960,10 +1960,10 @@ def test_crs_wkt_override_distinct_from_default(self, source_tif, tmp_path): ) # Override path vrt_override = str(tmp_path / 'override.vrt') - build_vrt(vrt_override, [source_tif], crs=override) + _build_vrt(vrt_override, [source_tif], crs=override) # Default path vrt_default = str(tmp_path / 'default.vrt') - build_vrt(vrt_default, [source_tif]) + _build_vrt(vrt_default, [source_tif]) with open(vrt_override, 'r') as fh: text_override = fh.read() @@ -1985,7 +1985,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') - build_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 @@ -1996,7 +1996,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') - build_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 @@ -2005,14 +2005,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') - build_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: - """``build_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.""" @@ -2020,12 +2020,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"): - build_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: - build_vrt(vrt_path, []) + _build_vrt(vrt_path, []) except ValueError: pass assert not os.path.exists(vrt_path) @@ -2617,7 +2617,7 @@ def _make_georef_single_tile_vrt(tmp_path, arr: np.ndarray) -> str: GeoTransform. ``_make_single_tile_vrt`` writes a bare numpy array, which produces a - non-georeferenced tile; build_vrt then omits the ```` + non-georeferenced tile; _build_vrt then omits the ```` (see issue #2966) and the VRT reads back with integer pixel coords. Tests that assert the windowed *transform* / float coordinate shift need a real GeoTransform pinned to origin ``(0, 0)`` so the @@ -2658,7 +2658,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 - build_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] @@ -2849,7 +2849,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 build_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') diff --git a/xrspatial/geotiff/tests/vrt/test_metadata.py b/xrspatial/geotiff/tests/vrt/test_metadata.py index 8f404eac..c88199c8 100644 --- a/xrspatial/geotiff/tests/vrt/test_metadata.py +++ b/xrspatial/geotiff/tests/vrt/test_metadata.py @@ -12,7 +12,7 @@ * ``mask_nodata=False`` preserves float sentinels * Tile-level metadata parity for VRT tiled writes * VRT XML parsed once on the chunked path -* ``build_vrt`` escapes XML special characters +* ``_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 @@ -32,7 +32,7 @@ import xarray as xr from xrspatial.geotiff import (GeoTIFFFallbackWarning, MixedBandMetadataError, _read_geotiff_dask, - _read_vrt, build_vrt, open_geotiff, to_geotiff) + _read_vrt, _build_vrt, open_geotiff, to_geotiff) from xrspatial.geotiff._attrs import (GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_TRANSFORM_ONLY) from xrspatial.geotiff._errors import VRTUnsupportedError @@ -509,7 +509,7 @@ 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') - build_vrt(vrt_path, [tif]) + _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 @@ -523,7 +523,7 @@ 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') - build_vrt(vrt_path, [tif]) + _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) @@ -537,7 +537,7 @@ 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') - build_vrt(vrt_path, [tif]) + _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]) @@ -547,7 +547,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 ``build_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 @@ -565,7 +565,7 @@ 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') - build_vrt(vrt_path, [tif]) + _build_vrt(vrt_path, [tif]) _int_nodata_rewrite_vrt_nodata(vrt_path, '1.9') 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 @@ -580,7 +580,7 @@ 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') - build_vrt(vrt_path, [tif]) + _build_vrt(vrt_path, [tif]) _int_nodata_rewrite_vrt_nodata(vrt_path, '-1') 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 @@ -594,7 +594,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') - build_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 @@ -1111,7 +1111,7 @@ def test_parsed_kwarg_does_not_mutate_caller_holes(single_parse_single_tile_vrt_ # --------------------------------------------------------------------------- -# build_vrt escapes XML special chars +# _build_vrt escapes XML special chars # --------------------------------------------------------------------------- @@ -1128,7 +1128,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 build_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\'"]' @@ -1175,7 +1175,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 build_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') @@ -1799,7 +1799,7 @@ def test_missing_sources_warn_records_holes(tmp_path): # --------------------------------------------------------------------------- -# Non-georeferenced sources: build_vrt must not fabricate a GeoTransform +# Non-georeferenced sources: _build_vrt must not fabricate a GeoTransform # (issue #2966) # --------------------------------------------------------------------------- @@ -1836,11 +1836,11 @@ def test_build_vrt_non_georef_source_omits_geotransform(tmp_path): assert open_geotiff(tif).attrs.get('georef_status') == GEOREF_STATUS_NONE vrt_path = str(tmp_path / 'plain_2966.vrt') - build_vrt(vrt_path, [tif]) + _build_vrt(vrt_path, [tif]) xml = pathlib.Path(vrt_path).read_text() assert '' not in xml, ( - 'build_vrt fabricated a GeoTransform for a non-georeferenced ' + '_build_vrt fabricated a GeoTransform for a non-georeferenced ' 'source: ' + xml ) @@ -1851,7 +1851,7 @@ def test_build_vrt_non_georef_source_preserves_status_none(tmp_path): tif, arr = _write_plain_tif_2966(tmp_path, 'plain_status_2966.tif') vrt_path = str(tmp_path / 'plain_status_2966.vrt') - build_vrt(vrt_path, [tif]) + _build_vrt(vrt_path, [tif]) out = open_geotiff(vrt_path) assert out.attrs.get('georef_status') == GEOREF_STATUS_NONE @@ -1865,7 +1865,7 @@ def test_build_vrt_georef_source_still_emits_geotransform(tmp_path): tif, _ = _write_georef_tif_2966(tmp_path, 'geo_2966.tif') vrt_path = str(tmp_path / 'geo_2966.vrt') - build_vrt(vrt_path, [tif]) + _build_vrt(vrt_path, [tif]) xml = pathlib.Path(vrt_path).read_text() assert '' in xml @@ -1880,4 +1880,4 @@ def test_build_vrt_mixed_georef_sources_rejected(tmp_path): vrt_path = str(tmp_path / 'mixed_2966.vrt') with pytest.raises(ValueError, match='mix georeferenced and non-georeferenced'): - build_vrt(vrt_path, [geo_tif, plain_tif]) + _build_vrt(vrt_path, [geo_tif, plain_tif]) diff --git a/xrspatial/geotiff/tests/vrt/test_validation.py b/xrspatial/geotiff/tests/vrt/test_validation.py index c4f49795..edff0130 100644 --- a/xrspatial/geotiff/tests/vrt/test_validation.py +++ b/xrspatial/geotiff/tests/vrt/test_validation.py @@ -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 build_vrt + from xrspatial.geotiff import _build_vrt vrt_path = tmp_path / "mosaic.vrt" - build_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) diff --git a/xrspatial/geotiff/tests/write/test_basic.py b/xrspatial/geotiff/tests/write/test_basic.py index 05f336fb..7e89cd90 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`` / ``build_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 @@ -38,13 +38,13 @@ from xrspatial.geotiff import _vrt as _vrt_module from xrspatial.geotiff import _write_geotiff_gpu from xrspatial.geotiff import _writer as writer_mod -from xrspatial.geotiff import build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _build_vrt, open_geotiff, to_geotiff 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 -# ``build_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). @@ -527,12 +527,12 @@ def test_to_geotiff_dask_streaming_returns_path(tmp_path): def test_write_vrt_returns_string_path(tmp_path): - """``build_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 = build_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) @@ -568,7 +568,7 @@ def test_writer_signatures_declare_path_return(): expected = { to_geotiff: "str | BinaryIO", _write_geotiff_gpu: "str | BinaryIO", - build_vrt: "str", + _build_vrt: "str", } for fn, expected_ann in expected.items(): sig = inspect.signature(fn) @@ -581,7 +581,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 ``build_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 @@ -596,7 +596,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 = build_vrt(str(tmp_path / "m.vrt"), [src]) + vrt_rv = _build_vrt(str(tmp_path / "m.vrt"), [src]) assert vrt_rv is not None @@ -773,7 +773,7 @@ def _wrapped(*args, **kwargs): # ------------------------------------------------------------------------- -# Section: build_vrt path kwarg contract +# Section: _build_vrt path kwarg contract # ------------------------------------------------------------------------- def _build_source_tif(tmp_path, name='src.tif'): @@ -797,7 +797,7 @@ def test_write_vrt_signature_first_arg_is_path(): 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(build_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. @@ -810,9 +810,9 @@ def test_write_vrt_signature_first_arg_is_path(): def test_write_vrt_positional_path_works(tmp_path): - """Positional ``build_vrt(path, sources)`` is unchanged. + """Positional ``_build_vrt(path, sources)`` is unchanged. - Existing callers ``build_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. """ @@ -820,24 +820,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 = build_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 ``build_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 - ``build_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 = build_vrt(path=out, source_files=[src]) + result = _build_vrt(path=out, source_files=[src]) assert result == out assert os.path.exists(out) @@ -852,7 +852,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 = build_vrt(vrt_path=out, source_files=[src]) + result = _build_vrt(vrt_path=out, source_files=[src]) assert result == out assert os.path.exists(out) @@ -861,13 +861,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 - build_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"): - build_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): @@ -881,11 +881,11 @@ def test_write_vrt_no_path_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match='path'): - build_vrt(source_files=[src]) + _build_vrt(source_files=[src]) def test_write_vrt_explicit_path_none_raises(tmp_path): - """``build_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". @@ -895,11 +895,11 @@ def test_write_vrt_explicit_path_none_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match="'path'.*None"): - build_vrt(path=None, source_files=[src]) + _build_vrt(path=None, source_files=[src]) def test_write_vrt_positional_none_raises(tmp_path): - """Positional ``build_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 @@ -909,7 +909,7 @@ def test_write_vrt_positional_none_raises(tmp_path): """ src = _build_source_tif(tmp_path) with pytest.raises(TypeError, match="'path'.*None"): - build_vrt(None, [src]) + _build_vrt(None, [src]) def test_write_vrt_first_arg_name_matches_writer_trio(): @@ -927,7 +927,7 @@ def test_write_vrt_first_arg_name_matches_writer_trio(): inspect.signature(_write_geotiff_gpu).parameters )[1] vrt_first = list( - inspect.signature(build_vrt).parameters + inspect.signature(_build_vrt).parameters )[0] # path, source_files -> index 0 assert eager_first == 'path' assert gpu_first == 'path' @@ -945,12 +945,12 @@ 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') - build_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) - build_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) @@ -958,7 +958,7 @@ def test_write_vrt_path_round_trip_matches_old(tmp_path): # ------------------------------------------------------------------------- -# Section: build_vrt CRS propagation +# Section: _build_vrt CRS propagation # ------------------------------------------------------------------------- @@ -969,7 +969,7 @@ def test_write_vrt_accepts_crs_kwarg(): """``crs`` is in the signature and defaults to ``None``.""" import inspect - sig = inspect.signature(build_vrt) + sig = inspect.signature(_build_vrt) assert 'crs' in sig.parameters assert sig.parameters['crs'].default is None @@ -980,7 +980,7 @@ def test_write_vrt_crs_annotation_matches_writer_trio(): """ import inspect - sig = inspect.signature(build_vrt) + sig = inspect.signature(_build_vrt) ann = str(sig.parameters['crs'].annotation) assert ann == 'int | str | None' @@ -1000,7 +1000,7 @@ def test_write_vrt_crs_epsg_int_writes_wkt_to_xml(tmp_path): src = _build_source_tif(tmp_path, 'epsg_int.tif') vrt_path = str(tmp_path / 'epsg_int.vrt') - out = build_vrt(vrt_path, [src], crs=4326) + out = _build_vrt(vrt_path, [src], crs=4326) assert out == vrt_path assert os.path.exists(vrt_path) @@ -1018,7 +1018,7 @@ def test_write_vrt_crs_wkt_string(tmp_path): wkt = CRS.from_epsg(4326).to_wkt() - out = build_vrt(vrt_path, [src], crs=wkt) + out = _build_vrt(vrt_path, [src], crs=wkt) assert out == vrt_path da = _read_vrt(vrt_path) # WKT round-trips back to EPSG:4326 via _wkt_to_epsg @@ -1032,7 +1032,7 @@ def test_write_vrt_crs_none_falls_through(tmp_path): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - out = build_vrt(vrt_path, [src], crs=None) + out = _build_vrt(vrt_path, [src], crs=None) assert out == vrt_path da = _read_vrt(vrt_path) # The source TIFF was written with EPSG:4326; VRT inherits it. @@ -1048,7 +1048,7 @@ def test_write_vrt_no_crs_kwarg_no_warning(tmp_path): with warnings.catch_warnings(): warnings.simplefilter('error', DeprecationWarning) - build_vrt(vrt_path, [src]) # neither kwarg supplied + _build_vrt(vrt_path, [src]) # neither kwarg supplied assert os.path.exists(vrt_path) @@ -1066,7 +1066,7 @@ 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 = build_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) assert da.attrs.get('crs') == 4326 @@ -1080,7 +1080,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'): - build_vrt(vrt_path, [src], crs_wkt=None) + _build_vrt(vrt_path, [src], crs_wkt=None) assert os.path.exists(vrt_path) @@ -1096,7 +1096,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'): - build_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 --- @@ -1108,9 +1108,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 _write_geotiff_gpu, build_vrt, to_geotiff + from xrspatial.geotiff import _write_geotiff_gpu, _build_vrt, to_geotiff - for fn in (to_geotiff, _write_geotiff_gpu, build_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 ( @@ -1128,7 +1128,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'): - build_vrt(vrt_path, [src], crs=[4326]) + _build_vrt(vrt_path, [src], crs=[4326]) def test_write_vrt_crs_unparseable_string_rejected(tmp_path): @@ -1139,11 +1139,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'): - build_vrt(vrt_path, [src], crs='not-a-real-crs-string') + _build_vrt(vrt_path, [src], crs='not-a-real-crs-string') # ------------------------------------------------------------------------- -# Section: build_vrt bool nodata +# Section: _build_vrt bool nodata # ------------------------------------------------------------------------- @pytest.fixture @@ -1155,14 +1155,14 @@ def uint8_da(): @pytest.fixture def src_geotiff(uint8_da, tmp_path): - """A real on-disk source GeoTIFF that build_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 # --------------------------------------------------------------------------- -# build_vrt: bool nodata rejection +# _build_vrt: bool nodata rejection # --------------------------------------------------------------------------- @@ -1171,15 +1171,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): - """``build_vrt`` raises ``TypeError`` for any bool nodata. + """``_build_vrt`` raises ``TypeError`` for any bool nodata. - The public ``build_vrt`` wrapper routes + The internal ``_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"): - build_vrt(vrt_path, [src_geotiff], nodata=bad) + _build_vrt(vrt_path, [src_geotiff], nodata=bad) @pytest.mark.parametrize( @@ -1210,7 +1210,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") - build_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 @@ -1222,7 +1222,7 @@ 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") - build_vrt(vrt_path, [src_geotiff], nodata=None) + _build_vrt(vrt_path, [src_geotiff], nodata=None) assert os.path.exists(vrt_path) @@ -1265,7 +1265,7 @@ def test_to_geotiff_gpu_dispatch_rejects_bool_nodata(uint8_da, tmp_path): # ------------------------------------------------------------------------- -# Section: build_vrt int nodata +# Section: _build_vrt int nodata # ------------------------------------------------------------------------- def _nodata_annotation(fn): @@ -1275,7 +1275,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(build_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). @@ -1324,7 +1324,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. - build_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( diff --git a/xrspatial/geotiff/tests/write/test_nodata.py b/xrspatial/geotiff/tests/write/test_nodata.py index db249057..2d5126d7 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 _read_geotiff_dask, _read_vrt, build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _read_geotiff_dask, _read_vrt, _build_vrt, open_geotiff, to_geotiff 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 @@ -431,7 +431,7 @@ def test_int64_max_masked_via_dask(self, tmp_path): class TestVrtRoundTrip: """write_vrt -> _read_vrt round-trip -- the path that surfaced the bug - in the wild (build_vrt stringifies geo_info.nodata into XML).""" + 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") - build_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. @@ -463,7 +463,7 @@ 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") - build_vrt(vrt_path, [tif_path]) + _build_vrt(vrt_path, [tif_path]) with open(vrt_path) as f: xml = f.read() From ffbdf604b755bbc1dcc507451ce090ac762a86f5 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 5 Jun 2026 12:21:59 -0700 Subject: [PATCH 2/4] Drop build_vrt from docs, README, and user-guide notebook (#2974) Remove the public build_vrt entry from the GeoTIFF reference, safe-IO user guide, release-gate contract table, README feature matrix, and the internal-dev notes. The user-guide notebook's VRT section now writes a mosaic through to_geotiff's .vrt path (chunked DataArray -> tiled GeoTIFFs + index) instead of calling the removed public helper; its output cell was regenerated from a real run. --- README.md | 7 +- docs/source/reference/geotiff.rst | 29 ++++---- docs/source/reference/geotiff_internals.md | 4 +- .../source/reference/release_gate_geotiff.rst | 5 +- docs/source/user_guide/geotiff_safe_io.rst | 3 - examples/user_guide/39_GeoTIFF_IO.ipynb | 67 +++++++------------ 6 files changed, 48 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index cf1bdee5..31698717 100644 --- a/README.md +++ b/README.md @@ -162,15 +162,14 @@ VRT is supported as a conservative advanced feature for simple GeoTIFF mosaics, | Name | Description | NumPy | Dask | CuPy GPU | Dask+CuPy GPU | Cloud | |:-----|:------------|:-----:|:----:|:--------:|:-------------:|:-----:| | [open_geotiff](xrspatial/geotiff/__init__.py) | Read GeoTIFF / COG / VRT | โœ… | โœ… | ๐Ÿงช | ๐Ÿงช | ๐Ÿ”ผ | -| [to_geotiff](xrspatial/geotiff/__init__.py) | Write DataArray as GeoTIFF / COG | โœ… | โœ… | ๐Ÿงช | ๐Ÿงช | ๐Ÿ”ผ | -| [build_vrt](xrspatial/geotiff/__init__.py) | Generate VRT mosaic from existing GeoTIFFs | ๐Ÿ”ผ | | | | | +| [to_geotiff](xrspatial/geotiff/__init__.py) | Write DataArray as GeoTIFF / COG / VRT | โœ… | โœ… | ๐Ÿงช | ๐Ÿงช | ๐Ÿ”ผ | `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 build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import open_geotiff, to_geotiff open_geotiff('dem.tif') # NumPy open_geotiff('dem.tif', chunks=512) # Dask @@ -187,7 +186,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') -build_vrt('mosaic.vrt', ['tile1.tif', 'tile2.tif']) # mosaic existing tiles +to_geotiff(data, 'mosaic.vrt') # write a tiled VRT mosaic 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 a2dfd2e5..3bd62a77 100644 --- a/docs/source/reference/geotiff.rst +++ b/docs/source/reference/geotiff.rst @@ -220,13 +220,13 @@ 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. +Writing to a ``.vrt`` path is how you produce a VRT mosaic; the underlying +index emitter is internal. .. autosummary:: :toctree: _autosummary xrspatial.geotiff.to_geotiff - xrspatial.geotiff.build_vrt COG validator CI gate ===================== @@ -400,9 +400,9 @@ VRT support matrix (issue #2321) VRT reads sit at the ``advanced`` tier in :data:`xrspatial.geotiff.SUPPORTED_FEATURES` (``reader.vrt``). -``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. +``open_geotiff`` (on a ``.vrt`` source) and ``to_geotiff`` (to a ``.vrt`` +output) both target the same narrow subset of GDAL's VRT spec. The +reference below is the canonical contract; the docstrings echo it. Supported --------- @@ -452,23 +452,22 @@ Non-goals (intentionally unsupported) Safe usage ---------- -A simple mosaic over two compatible GeoTIFF tiles, read eagerly with -the fail-closed defaults: +Write a chunked DataArray to a ``.vrt`` path (``to_geotiff`` tiles the +array and emits the index), then read it back eagerly with the +fail-closed defaults: .. code-block:: python - from xrspatial.geotiff import build_vrt, open_geotiff + from xrspatial.geotiff import open_geotiff, to_geotiff - # Write a VRT that mosaics two tiles. Both tiles share CRS, - # pixel size, dtype, and band count. - vrt_path = build_vrt( - 'mosaic.vrt', - source_files=['tile_west.tif', 'tile_east.tif'], - ) + # ``da`` is a 2D dask-backed DataArray with crs / transform set. + # Writing to a .vrt produces a directory of tiled GeoTIFFs plus + # the VRT index that references them. + to_geotiff(da, 'mosaic.vrt') # Read with the defaults: missing_sources='raise', # band_nodata=None (fail closed on disagreeing per-band sentinels). - da = open_geotiff(vrt_path) + back = open_geotiff('mosaic.vrt') Intentionally raises -------------------- diff --git a/docs/source/reference/geotiff_internals.md b/docs/source/reference/geotiff_internals.md index 99cd4ff6..4a916410 100644 --- a/docs/source/reference/geotiff_internals.md +++ b/docs/source/reference/geotiff_internals.md @@ -28,7 +28,7 @@ public API. Files referenced live under `xrspatial/geotiff/`. | -------------------- | --------------------------------- | ---------------------- | | `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 | -| `build_vrt` | `xrspatial/geotiff/_writers/vrt.py` | list of GeoTIFF paths (XML emitter) | +| `_build_vrt` | `xrspatial/geotiff/_writers/vrt.py` | list of GeoTIFF paths (XML emitter; internal, reached by `to_geotiff`'s `.vrt` path) | ## Contract steps @@ -142,7 +142,7 @@ 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` | `build_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 07e3dafe..099eb120 100644 --- a/docs/source/reference/release_gate_geotiff.rst +++ b/docs/source/reference/release_gate_geotiff.rst @@ -557,9 +557,10 @@ VRT supported subset - ``xrspatial/geotiff/tests/release_gates/test_stable_features.py`` (VRT presence meta-gate) - `#2321`_ - * - ``build_vrt`` + * - VRT write (``.vrt`` output) - advanced - - Writer rejects source-incompatibility cases at the writer boundary. + - Writer rejects source-incompatibility cases at the writer boundary + (``to_geotiff`` to a ``.vrt`` path via the internal ``_build_vrt``). - ``xrspatial/geotiff/tests/vrt/test_validation.py`` - `#2342`_ diff --git a/docs/source/user_guide/geotiff_safe_io.rst b/docs/source/user_guide/geotiff_safe_io.rst index a2661d85..9b908e97 100644 --- a/docs/source/user_guide/geotiff_safe_io.rst +++ b/docs/source/user_guide/geotiff_safe_io.rst @@ -54,9 +54,6 @@ the read and write paths: 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 7eaea59e..abeeab05 100644 --- a/examples/user_guide/39_GeoTIFF_IO.ipynb +++ b/examples/user_guide/39_GeoTIFF_IO.ipynb @@ -13,25 +13,14 @@ "cell_type": "markdown", "metadata": {}, "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 `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" + "### 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 writes a `.vrt` with `to_geotiff` 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" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### What you'll build\n", - "\n", - "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 build_vrt](#VRT-mosaic) to build a virtual mosaic from multiple GeoTIFFs\n", - "\n", - "![GeoTIFF I/O preview](images/geotiff_io_preview.png)" + "### What you'll build\n\n1. [Write and read back a GeoTIFF](#Write-and-read-back) with `to_geotiff` and `open_geotiff`\n2. [Write from a DataArray accessor](#Accessor-write) using `da.xrs.to_geotiff()`\n3. [Windowed read via Dataset accessor](#Windowed-read-via-Dataset) using `ds.xrs.open_geotiff()` to crop a large file to an existing spatial extent\n4. [Write a VRT mosaic with to_geotiff](#VRT-mosaic) by writing a chunked DataArray to a `.vrt` path\n\n![GeoTIFF I/O preview](images/geotiff_io_preview.png)" ] }, { @@ -64,7 +53,7 @@ "import matplotlib.pyplot as plt\n", "\n", "import xrspatial\n", - "from xrspatial.geotiff import open_geotiff, to_geotiff, build_vrt" + "from xrspatial.geotiff import open_geotiff, to_geotiff" ] }, { @@ -272,7 +261,7 @@ "}\n", "\n", ".xr-group-name::before {\n", - " content: \"\ud83d\udcc1\";\n", + " content: \"๐Ÿ“\";\n", " padding-right: 0.3em;\n", "}\n", "\n", @@ -335,7 +324,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: \"\u25ba\";\n", + " content: \"โ–บ\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -346,7 +335,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: \"\u25bc\";\n", + " content: \"โ–ผ\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -918,7 +907,7 @@ "source": [ "## VRT mosaic\n", "\n", - "`build_vrt` writes a lightweight XML file that stitches multiple GeoTIFFs into one virtual raster. The tiles aren't copied, just referenced." + "Writing a chunked DataArray to a `.vrt` path tiles it into separate GeoTIFFs and writes a lightweight XML index that stitches them into one virtual raster. The tiles aren't copied into the index, just referenced." ] }, { @@ -937,40 +926,36 @@ "name": "stdout", "output_type": "stream", "text": [ - "nw: (100, 150) -> 54,041 bytes\n", - "ne: (100, 150) -> 54,052 bytes\n", - "sw: (100, 150) -> 54,090 bytes\n", - "se: (100, 150) -> 54,051 bytes\n", + "VRT: 2,452 bytes\n", + "tile_00_00.tif: 53,127 bytes\n", + "tile_00_01.tif: 53,140 bytes\n", + "tile_01_00.tif: 53,174 bytes\n", + "tile_01_01.tif: 53,162 bytes\n", "\n", - "VRT: 2,178 bytes\n", "Mosaic shape: (200, 300)\n", "Matches original: True\n" ] } ], "source": [ - "# Split into 4 tiles and write each\n", - "tiles = [\n", - " ('nw', da[:100, :150]),\n", - " ('ne', da[:100, 150:]),\n", - " ('sw', da[100:, :150]),\n", - " ('se', da[100:, 150:]),\n", - "]\n", - "tile_paths = []\n", - "for name, tile in tiles:\n", - " p = os.path.join(tmpdir, f'tile_{name}.tif')\n", - " to_geotiff(tile, p, compression='deflate')\n", - " tile_paths.append(p)\n", - " print(f'{name}: {tile.shape} -> {os.path.getsize(p):,} bytes')\n", + "# Writing to a .vrt path tiles the array and emits an XML index that\n", + "# references the tiles. Chunk the array first so the mosaic spans\n", + "# multiple tiles (a 2x2 grid here).\n", + "chunked = da.chunk({'y': 100, 'x': 150})\n", "\n", - "# Stitch into a VRT\n", "vrt_path = os.path.join(tmpdir, 'mosaic.vrt')\n", - "build_vrt(vrt_path, tile_paths)\n", - "print(f'\\nVRT: {os.path.getsize(vrt_path):,} bytes')\n", + "to_geotiff(chunked, vrt_path)\n", + "print(f'VRT: {os.path.getsize(vrt_path):,} bytes')\n", + "\n", + "# The tiles land in a sibling *_tiles directory next to the .vrt\n", + "tiles_dir = os.path.join(tmpdir, 'mosaic_tiles')\n", + "for name in sorted(os.listdir(tiles_dir)):\n", + " size = os.path.getsize(os.path.join(tiles_dir, name))\n", + " print(f'{name}: {size:,} bytes')\n", "\n", "# Read the mosaic back\n", "mosaic = open_geotiff(vrt_path)\n", - "print(f'Mosaic shape: {mosaic.shape}')\n", + "print(f'\\nMosaic shape: {mosaic.shape}')\n", "print(f'Matches original: {np.allclose(mosaic.values, da.values)}')" ] }, @@ -978,7 +963,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The VRT is a few hundred bytes of XML. `open_geotiff` assembles the tiles when you read it." + "The VRT is a small XML index. `open_geotiff` assembles the tiles when you read it." ] }, { From 52ce6f356da56ac9d4f20a67cc38b500a2e3a876 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 5 Jun 2026 12:23:08 -0700 Subject: [PATCH 3/4] Fix isort ordering and continuation indent after _build_vrt rename (#2974) --- xrspatial/geotiff/tests/integration/test_http_sources.py | 5 +++-- xrspatial/geotiff/tests/parity/test_backend_matrix.py | 2 +- xrspatial/geotiff/tests/parity/test_finalization.py | 2 +- xrspatial/geotiff/tests/parity/test_pixel_equality.py | 2 +- xrspatial/geotiff/tests/parity/test_signature_contract.py | 5 +++-- xrspatial/geotiff/tests/test_polish.py | 4 ++-- xrspatial/geotiff/tests/unit/test_signatures.py | 6 +++--- xrspatial/geotiff/tests/vrt/test_metadata.py | 4 ++-- xrspatial/geotiff/tests/write/test_basic.py | 6 +++--- xrspatial/geotiff/tests/write/test_nodata.py | 2 +- 10 files changed, 20 insertions(+), 18 deletions(-) diff --git a/xrspatial/geotiff/tests/integration/test_http_sources.py b/xrspatial/geotiff/tests/integration/test_http_sources.py index 182e32fd..5462d570 100644 --- a/xrspatial/geotiff/tests/integration/test_http_sources.py +++ b/xrspatial/geotiff/tests/integration/test_http_sources.py @@ -21,10 +21,11 @@ import pytest import xarray as xr -from xrspatial.geotiff import UnsafeURLError, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt +from xrspatial.geotiff import (UnsafeURLError, _build_vrt, _read_geotiff_dask, _read_geotiff_gpu, + _read_vrt) from xrspatial.geotiff import _reader as _reader_mod from xrspatial.geotiff import _sources as _sources_mod -from xrspatial.geotiff import _write_geotiff_gpu, _build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _write_geotiff_gpu, open_geotiff, to_geotiff 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, diff --git a/xrspatial/geotiff/tests/parity/test_backend_matrix.py b/xrspatial/geotiff/tests/parity/test_backend_matrix.py index 7b942377..4a19ed2d 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 _read_vrt, _build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _build_vrt, _read_vrt, open_geotiff, to_geotiff from xrspatial.geotiff._attrs import _finalize_eager_read, _finalize_lazy_read_attrs from xrspatial.geotiff._errors import RotatedTransformError, UnparseableCRSError diff --git a/xrspatial/geotiff/tests/parity/test_finalization.py b/xrspatial/geotiff/tests/parity/test_finalization.py index 2b210c91..fad11ba4 100644 --- a/xrspatial/geotiff/tests/parity/test_finalization.py +++ b/xrspatial/geotiff/tests/parity/test_finalization.py @@ -35,7 +35,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _build_vrt, +from xrspatial.geotiff import (_build_vrt, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, open_geotiff, to_geotiff) from xrspatial.geotiff._attrs import (GEOREF_STATUS_CRS_ONLY, GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_ROTATED_DROPPED, diff --git a/xrspatial/geotiff/tests/parity/test_pixel_equality.py b/xrspatial/geotiff/tests/parity/test_pixel_equality.py index 787bf259..2e610b54 100644 --- a/xrspatial/geotiff/tests/parity/test_pixel_equality.py +++ b/xrspatial/geotiff/tests/parity/test_pixel_equality.py @@ -30,7 +30,7 @@ import pytest import xarray as xr -from xrspatial.geotiff import (_read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _build_vrt, +from xrspatial.geotiff import (_build_vrt, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, open_geotiff, to_geotiff) from .._helpers.markers import gpu_available, requires_gpu, requires_loopback diff --git a/xrspatial/geotiff/tests/parity/test_signature_contract.py b/xrspatial/geotiff/tests/parity/test_signature_contract.py index 67bd2071..a588d081 100644 --- a/xrspatial/geotiff/tests/parity/test_signature_contract.py +++ b/xrspatial/geotiff/tests/parity/test_signature_contract.py @@ -35,8 +35,9 @@ import pytest import xarray as xr -from xrspatial.geotiff import (SUPPORTED_FEATURES, _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, - _write_geotiff_gpu, _build_vrt, open_geotiff, to_geotiff) +from xrspatial.geotiff import (SUPPORTED_FEATURES, _build_vrt, _read_geotiff_dask, + _read_geotiff_gpu, _read_vrt, _write_geotiff_gpu, open_geotiff, + to_geotiff) from .._helpers.markers import requires_gpu diff --git a/xrspatial/geotiff/tests/test_polish.py b/xrspatial/geotiff/tests/test_polish.py index cfea4f8a..fee8eaa5 100644 --- a/xrspatial/geotiff/tests/test_polish.py +++ b/xrspatial/geotiff/tests/test_polish.py @@ -23,7 +23,7 @@ import numpy as np import pytest -from xrspatial.geotiff import _read_geotiff_dask, _build_vrt, to_geotiff +from xrspatial.geotiff import _build_vrt, _read_geotiff_dask, to_geotiff from xrspatial.geotiff._reader import _MmapCache, read_to_array from xrspatial.geotiff._writer import _MAX_OVERVIEW_LEVELS, write @@ -94,7 +94,7 @@ def test_known_kwargs_accepted(self, tmp_path): # ``crs=None`` instead of the deprecated alias to avoid the # DeprecationWarning the alias now emits. _build_vrt(vrt_path, [a_path], relative=False, crs=None, - nodata=-9999.0) + nodata=-9999.0) assert os.path.exists(vrt_path) def test_unknown_kwarg_raises_typeerror(self, tmp_path): diff --git a/xrspatial/geotiff/tests/unit/test_signatures.py b/xrspatial/geotiff/tests/unit/test_signatures.py index f6b65892..9220ea56 100644 --- a/xrspatial/geotiff/tests/unit/test_signatures.py +++ b/xrspatial/geotiff/tests/unit/test_signatures.py @@ -62,9 +62,9 @@ import xrspatial.geotiff as g import xrspatial.geotiff._compression as comp_mod -from xrspatial.geotiff import (GeoTIFFFallbackWarning, _geotiff_strict_mode, _read_geotiff_dask, - _read_geotiff_gpu, _read_vrt, _wkt_to_epsg, _write_geotiff_gpu, - _build_vrt, open_geotiff, to_geotiff) +from xrspatial.geotiff import (GeoTIFFFallbackWarning, _build_vrt, _geotiff_strict_mode, + _read_geotiff_dask, _read_geotiff_gpu, _read_vrt, _wkt_to_epsg, + _write_geotiff_gpu, open_geotiff, to_geotiff) 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, diff --git a/xrspatial/geotiff/tests/vrt/test_metadata.py b/xrspatial/geotiff/tests/vrt/test_metadata.py index c88199c8..940602f6 100644 --- a/xrspatial/geotiff/tests/vrt/test_metadata.py +++ b/xrspatial/geotiff/tests/vrt/test_metadata.py @@ -31,8 +31,8 @@ import pytest import xarray as xr -from xrspatial.geotiff import (GeoTIFFFallbackWarning, MixedBandMetadataError, _read_geotiff_dask, - _read_vrt, _build_vrt, open_geotiff, to_geotiff) +from xrspatial.geotiff import (GeoTIFFFallbackWarning, MixedBandMetadataError, _build_vrt, + _read_geotiff_dask, _read_vrt, open_geotiff, to_geotiff) from xrspatial.geotiff._attrs import (GEOREF_STATUS_FULL, GEOREF_STATUS_NONE, GEOREF_STATUS_TRANSFORM_ONLY) from xrspatial.geotiff._errors import VRTUnsupportedError diff --git a/xrspatial/geotiff/tests/write/test_basic.py b/xrspatial/geotiff/tests/write/test_basic.py index 7e89cd90..991d5e3e 100644 --- a/xrspatial/geotiff/tests/write/test_basic.py +++ b/xrspatial/geotiff/tests/write/test_basic.py @@ -34,11 +34,11 @@ import pytest import xarray as xr -from xrspatial.geotiff import _read_vrt +from xrspatial.geotiff import _build_vrt, _read_vrt from xrspatial.geotiff import _vrt as _vrt_module from xrspatial.geotiff import _write_geotiff_gpu from xrspatial.geotiff import _writer as writer_mod -from xrspatial.geotiff import _build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import open_geotiff, to_geotiff from xrspatial.geotiff._compression import COMPRESSION_NONE from xrspatial.geotiff._geotags import GeoTransform from xrspatial.geotiff._header import TAG_PHOTOMETRIC, parse_header, parse_ifd @@ -1108,7 +1108,7 @@ def test_writer_trio_all_accept_crs_kwarg(): output extension never has to special-case the kwarg name.""" import inspect - from xrspatial.geotiff import _write_geotiff_gpu, _build_vrt, to_geotiff + from xrspatial.geotiff import _build_vrt, _write_geotiff_gpu, to_geotiff for fn in (to_geotiff, _write_geotiff_gpu, _build_vrt): sig = inspect.signature(fn) diff --git a/xrspatial/geotiff/tests/write/test_nodata.py b/xrspatial/geotiff/tests/write/test_nodata.py index 2d5126d7..26bd4c57 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 _read_geotiff_dask, _read_vrt, _build_vrt, open_geotiff, to_geotiff +from xrspatial.geotiff import _build_vrt, _read_geotiff_dask, _read_vrt, open_geotiff, to_geotiff 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 From d04a25266a491b1809d16f2c908de88b7ca03209 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 5 Jun 2026 12:25:39 -0700 Subject: [PATCH 4/4] Address review nit: normalize _build_vrt docstring tier tags to internal-only (#2974) --- xrspatial/geotiff/_writers/vrt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xrspatial/geotiff/_writers/vrt.py b/xrspatial/geotiff/_writers/vrt.py index fa6ba143..dc71d23a 100644 --- a/xrspatial/geotiff/_writers/vrt.py +++ b/xrspatial/geotiff/_writers/vrt.py @@ -55,11 +55,11 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, Parameters ---------- path : str - [advanced] Output .vrt file path. Mirrors the ``path`` kwarg + [internal-only] Output .vrt file path. Mirrors the ``path`` kwarg 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. + [internal-only] Paths to the source GeoTIFF files. vrt_path : str, optional [internal-only] Deprecated alias for ``path``. Emits ``DeprecationWarning`` when supplied; passing both ``path`` @@ -68,10 +68,10 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, ``_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 + [internal-only] Store source paths relative to the VRT file (default True). crs : int, str, or None, optional - [advanced] EPSG code (int), WKT string, or PROJ string. If + [internal-only] 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`` so the same value can be forwarded to whichever writer the @@ -87,7 +87,7 @@ def _build_vrt(path: str = _VRT_PATH_MISSING_SENTINEL, ``str | None`` surface is preserved; new code should use ``crs`` instead, which additionally accepts ``int`` EPSG codes. nodata : float, int, or None, optional - [advanced] NoData value. If None, taken from the first source + [internal-only] NoData value. If None, taken from the first source GeoTIFF. Integer sentinels (e.g. ``65535`` for uint16, ``-9999`` for int32) are accepted so the surface lines up with the