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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
MixedBandMetadataError,
NonRepresentableEPSGCRSError, NonUniformCoordsError, RotatedTransformError,
UnknownCRSModelTypeError,
UnparseableCRSError, UnsupportedGeoTIFFFeatureError)
UnparseableCRSError, UnsupportedGeoTIFFFeatureError,
VRTStableSourcesOnlyError)
from ._geotags import RASTER_PIXEL_IS_AREA, RASTER_PIXEL_IS_POINT, GeoTransform # noqa: F401
from ._reader import _MAX_CLOUD_BYTES_SENTINEL, CloudSizeLimitError, UnsafeURLError
from ._reader import read_to_array as _read_to_array
Expand Down Expand Up @@ -122,6 +123,7 @@
'UnparseableCRSError',
'UnsafeURLError',
'UnsupportedGeoTIFFFeatureError',
'VRTStableSourcesOnlyError',
'open_geotiff',
'read_geotiff_gpu',
'read_geotiff_dask',
Expand Down Expand Up @@ -389,6 +391,7 @@ def open_geotiff(source: str | BinaryIO, *,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_invalid_nodata: bool = False,
stable_only: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -611,6 +614,19 @@ def open_geotiff(source: str | BinaryIO, *,
pre-rejection no-op behaviour for files known to carry such
sentinels (e.g. external tooling that writes ``"nan"`` on
integer outputs). See issue #2441 (#1774 follow-up).
stable_only : bool, default False
[advanced] Read-side opt-in for stable-tier sources only. When
``True``, a ``.vrt`` source raises
:class:`VRTStableSourcesOnlyError` because ``reader.vrt`` and
the VRT child-source pipeline sit at the ``advanced`` /
``experimental`` tiers in
:data:`xrspatial.geotiff.SUPPORTED_FEATURES`. Non-VRT sources
on this entry point already ride the stable ``reader.local_file``
path and the per-source codec gate, so the flag is a no-op for
them. The rejection names the file path and the
``allow_experimental_codecs`` opt-in so the caller can unlock
the broader tier set explicitly when needed. See epic #2342
and ``docs/source/reference/release_gate_geotiff.rst``.
allow_experimental_codecs : bool, default False
Read-side opt-in for sources compressed with the Tier 3
experimental codecs (``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``).
Expand Down Expand Up @@ -761,6 +777,7 @@ def open_geotiff(source: str | BinaryIO, *,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_invalid_nodata=allow_invalid_nodata,
stable_only=stable_only,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
band_nodata=band_nodata,
Expand All @@ -786,6 +803,7 @@ def open_geotiff(source: str | BinaryIO, *,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_invalid_nodata=allow_invalid_nodata,
stable_only=stable_only,
allow_experimental_codecs=(
allow_experimental_codecs),
allow_internal_only_jpeg=(
Expand All @@ -804,6 +822,7 @@ def open_geotiff(source: str | BinaryIO, *,
allow_inconsistent_geokeys=(
allow_inconsistent_geokeys),
allow_invalid_nodata=allow_invalid_nodata,
stable_only=stable_only,
allow_experimental_codecs=(
allow_experimental_codecs),
allow_internal_only_jpeg=(
Expand Down
11 changes: 11 additions & 0 deletions xrspatial/geotiff/_backends/dask.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def read_geotiff_dask(source: str, *,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_invalid_nodata: bool = False,
stable_only: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -142,6 +143,13 @@ def read_geotiff_dask(source: str, *,
``InvalidIntegerNodataError`` at graph-build time. See
``open_geotiff`` for the full description (#1774 follow-up,
#2441).
stable_only : bool, default False
[advanced] Read-side opt-in for stable-tier sources only.
Forwarded to ``read_vrt`` when the source ends in ``.vrt`` so
the rejection fires at graph-build time. Non-VRT sources on
this entry point already ride the stable ``reader.local_file``
path, so the flag is a no-op for them. See ``open_geotiff`` for
the full description (epic #2342).
allow_experimental_codecs : bool, default False
[advanced] Read-side opt-in for Tier 3 experimental codecs
(``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``). Fires at graph
Expand Down Expand Up @@ -227,6 +235,9 @@ def read_geotiff_dask(source: str, *,
allow_unparseable_crs=allow_unparseable_crs,
allow_inconsistent_geokeys=allow_inconsistent_geokeys,
allow_invalid_nodata=allow_invalid_nodata,
stable_only=stable_only,
allow_experimental_codecs=allow_experimental_codecs,
allow_internal_only_jpeg=allow_internal_only_jpeg,
band_nodata=band_nodata,
mask_nodata=mask_nodata,
**vrt_kwargs,
Expand Down
8 changes: 8 additions & 0 deletions xrspatial/geotiff/_backends/gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def read_geotiff_gpu(source: str, *,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_invalid_nodata: bool = False,
stable_only: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -217,6 +218,13 @@ def read_geotiff_gpu(source: str, *,
eager and dask paths; default raises
``InvalidIntegerNodataError``. See ``open_geotiff`` for the full
description (#1774 follow-up, #2441).
stable_only : bool, default False
[experimental] Read-side opt-in for stable-tier sources only.
The GPU read path does not consume VRT sources directly (VRT
routing happens in ``open_geotiff``), so this kwarg is accepted
for cross-backend signature symmetry and is a no-op on the GPU
eager / chunked paths. See ``open_geotiff`` for the full
description (epic #2342).
allow_experimental_codecs : bool, default False
[experimental] Read-side opt-in for Tier 3 experimental codecs
(``lerc``, ``jpeg2000`` / ``j2k``, ``lz4``). The GPU read path
Expand Down
28 changes: 28 additions & 0 deletions xrspatial/geotiff/_backends/vrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def read_vrt(source: str, *,
allow_unparseable_crs: bool = False,
allow_inconsistent_geokeys: bool = False,
allow_invalid_nodata: bool = False,
stable_only: bool = False,
allow_experimental_codecs: bool = False,
allow_internal_only_jpeg: bool = False,
band_nodata: str | None = None,
Expand Down Expand Up @@ -276,6 +277,17 @@ def read_vrt(source: str, *,
the per-source GeoTIFF reads built by the VRT planner. See
``open_geotiff`` for the full description (#1774 follow-up,
#2441).
stable_only : bool, default False
[advanced] Read-side opt-in for stable-tier sources only. When
``True``, ``read_vrt`` raises :class:`VRTStableSourcesOnlyError`
before any pixel decode because ``reader.vrt`` itself sits at
the ``advanced`` tier in :data:`SUPPORTED_FEATURES` and VRT
child sources can declare any codec the GeoTIFF reader supports
(including experimental and internal-only tiers). The message
names the file path and the ``allow_experimental_codecs``
unlock so the caller can opt into the broader tier set
explicitly. See epic #2342 and
``docs/source/reference/release_gate_geotiff.rst``.
allow_experimental_codecs : bool, default False
[advanced] Read-side opt-in for Tier 3 experimental codecs in
any source file referenced by the VRT. Forwarded to the
Expand Down Expand Up @@ -373,6 +385,22 @@ def read_vrt(source: str, *,

source = _coerce_path(source)

# Epic #2342: reject the read up front when the caller asked for
# stable-only sources. ``reader.vrt`` sits at the ``advanced`` tier
# and VRT children can declare any codec the GeoTIFF reader
# supports, so a stable-only request cannot be served from a VRT
# mosaic without the documented ``allow_experimental_codecs``
# unlock. Runs before the dispatcher-kwarg validator so the typed
# error surfaces before any other validation noise (a malformed VRT
# path, an unsupported ``overview_level``, etc.) competes for the
# raise site.
from .._validation import _validate_stable_only_vrt
_validate_stable_only_vrt(
source,
stable_only=stable_only,
allow_experimental_codecs=allow_experimental_codecs,
)

# Shared dispatcher-kwarg validator so direct callers see the same
# rejections as ``open_geotiff`` (issue #2175 / parent #2162). For
# ``read_vrt`` the helper rejects ``on_gpu_failure`` (VRT reads do
Expand Down
21 changes: 21 additions & 0 deletions xrspatial/geotiff/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,26 @@ class InvalidIntegerNodataError(GeoTIFFAmbiguousMetadataError):
"""


class VRTStableSourcesOnlyError(GeoTIFFAmbiguousMetadataError):
"""VRT source opened under ``stable_only=True`` (epic #2342).

Raised when a caller opens a ``.vrt`` file with ``stable_only=True``.
The VRT reader (``reader.vrt``) and its child sources sit at the
``advanced`` / ``experimental`` tiers in
:data:`xrspatial.geotiff.SUPPORTED_FEATURES`, so a request for
stable-only sources cannot be served from a VRT mosaic without an
explicit opt-in. The message names the offending VRT path and the
matching opt-in flag (``allow_experimental_codecs``) so the caller
learns the unlock at the boundary rather than from the docs.

Pass ``stable_only=False`` (the default) to keep the legacy
behaviour, or pass ``allow_experimental_codecs=True`` to opt into
the broader tier set explicitly. See the release contract document
at ``docs/source/reference/release_gate_geotiff.rst`` and epic
#2342 for the full rationale.
"""


class UnknownCRSModelTypeError(GeoTIFFAmbiguousMetadataError):
"""Can't classify an EPSG as geographic or projected on write (#2277).

Expand Down Expand Up @@ -230,6 +250,7 @@ class UnsupportedGeoTIFFFeatureError(ValueError):
"ConflictingCRSError",
"ConflictingNodataError",
"InvalidIntegerNodataError",
"VRTStableSourcesOnlyError",
"VRTUnsupportedError",
"UnknownCRSModelTypeError",
"NonRepresentableEPSGCRSError",
Expand Down
79 changes: 78 additions & 1 deletion xrspatial/geotiff/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from ._coords import _BAND_DIM_NAMES
from ._errors import (ConflictingCRSError, ConflictingNodataError, InconsistentGeoKeysError,
InvalidIntegerNodataError, MixedBandMetadataError, NonUniformCoordsError,
RotatedTransformError, UnparseableCRSError)
RotatedTransformError, UnparseableCRSError, VRTStableSourcesOnlyError)
from ._runtime import (_MISSING_SOURCES_SENTINEL, _ON_GPU_FAILURE_SENTINEL, _TIME_DIM_NAMES,
_X_DIM_NAMES, _Y_DIM_NAMES)

Expand Down Expand Up @@ -668,6 +668,83 @@ def _validate_int_nodata_for_dtype(
)


def _validate_stable_only_vrt(
source: str,
*,
stable_only: bool,
allow_experimental_codecs: bool = False,
) -> None:
"""Reject a VRT source when the caller asks for stable-only sources.

Implements the read-side gate the release-contract test
``test_release_gate_negative_mixed_tier_vrt_children`` pins from
epic #2342. The ``reader.vrt`` entry point sits at the ``advanced``
tier in :data:`xrspatial.geotiff.SUPPORTED_FEATURES`, and VRT child
sources can declare any codec / capability the underlying GeoTIFF
reader supports (including the ``experimental`` and ``internal_only``
tiers). A caller who asks for stable-only sources via
``stable_only=True`` therefore cannot be served from a VRT mosaic
without naming the broader-tier opt-in (``allow_experimental_codecs``).
Reject up front at graph-build / eager-read setup time so the
failure surfaces before any pixel decode work.

Parameters
----------
source : str
Path to the ``.vrt`` file. Embedded in the rejection message so
the caller can locate the offending file without re-parsing.
Non-string sources (eager file-like buffers) and string paths
that do not end in ``.vrt`` are silently passed through; the
gate only fires for sources the caller could reasonably have
intended as a VRT mosaic, so a future call site that routes a
non-VRT path through this helper does not mislabel the failure.
stable_only : bool
Caller's opt-in for stable-only sources. ``False`` is a no-op;
``True`` raises :class:`VRTStableSourcesOnlyError` (provided
``source`` looks like a VRT path).
allow_experimental_codecs : bool, default False
Companion opt-in. When the caller passes both ``stable_only=True``
and ``allow_experimental_codecs=True`` the request is internally
contradictory, but ``allow_experimental_codecs=True`` is the
documented unlock so we honour it: the gate becomes a no-op and
the per-source codec gate downstream handles the rest. Pure
``stable_only=True`` (without the unlock) raises.

Raises
------
VRTStableSourcesOnlyError
When ``stable_only=True`` and the source path ends in ``.vrt``
(case-insensitive) and the caller did not pass
``allow_experimental_codecs=True``. The message names the
offending VRT path, both flags, and cites the release-contract
document plus epic #2342.
"""
if not stable_only:
return
if allow_experimental_codecs:
return
# Defensive extension check: every public-API call site routes only
# ``.vrt`` paths into this helper, but a future call site could
# forward a non-VRT path. Pass through silently in that case so the
# rejection message never mislabels a non-VRT source as a VRT.
if not (isinstance(source, str) and source.lower().endswith('.vrt')):
return
raise VRTStableSourcesOnlyError(
f"VRT source '{source}' cannot be opened under stable_only=True. "
f"The VRT reader (``reader.vrt``) sits at the advanced tier in "
f"SUPPORTED_FEATURES, and VRT child sources can declare any "
f"codec or capability the GeoTIFF reader supports, including "
f"the experimental and internal-only tiers. The stable-only "
f"request cannot be served from a VRT mosaic without an "
f"explicit broader-tier opt-in. Pass "
f"allow_experimental_codecs=True to opt in to the advanced / "
f"experimental tiers, or drop stable_only=True to keep the "
f"default behaviour. See "
f"docs/source/reference/release_gate_geotiff.rst for the "
f"release contract and epic #2342 for the tracking issue."
)


def _validate_no_rotated_affine(attrs, *, drop_rotation: bool,
entry_point: str = "to_geotiff") -> None:
"""Refuse writes that would silently drop ``attrs['rotated_affine']``.
Expand Down
29 changes: 7 additions & 22 deletions xrspatial/geotiff/tests/release_gates/test_stable_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -2415,31 +2415,16 @@ def test_release_gate_negative_rotated_gpu(


@pytest.mark.release_gate
@pytest.mark.xfail(
reason=(
"The VRT stable-only knob is owned by epic #2342 and has not "
"landed yet. The release promise: when the caller asks for "
"stable-only sources and a VRT child uses an experimental codec, "
"the reader names the offending child and the opt-in flag. This "
"xfail flips to a pass when #2342 ships the knob."
),
strict=False,
)
def test_release_gate_negative_mixed_tier_vrt_children(tmp_path) -> None:
"""The reader must refuse mixed-tier VRT children when stable-only is asked.

XFAIL-to-PASS transition note
-----------------------------
Today this test fails with ``TypeError: unexpected keyword argument
'stable_only'`` because epic #2342 has not landed the kwarg yet. The
strict=False xfail swallows that TypeError. When #2342 lands, the
test will start raising :class:`GeoTIFFAmbiguousMetadataError` (or
fail to raise) and the xfail will report XPASS. Before removing the
xfail marker, confirm the new code path satisfies both inline
assertions: the error message must mention either ``stable_only`` or
``allow_experimental_codecs``, and it must cite the release contract
docs. If either assertion would not pass, fix the production message
in the same PR that removes the xfail.
Pinned by epic #2342 / issue #2443: when the caller asks for
stable-only sources via ``stable_only=True`` and the source is a
VRT, the read raises :class:`VRTStableSourcesOnlyError` (a
:class:`GeoTIFFAmbiguousMetadataError` subclass) before any pixel
decode. The message must name either ``stable_only`` or
``allow_experimental_codecs`` and cite the release-contract docs
or the tracking issue.
"""
path = _neg_tmp(tmp_path, "case4_mixed_tier_vrt", suffix=".vrt")
Path(path).write_text(
Expand Down
5 changes: 5 additions & 0 deletions xrspatial/geotiff/tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,11 @@ def test_all_lists_supported_functions(self):
# pansharpened / derived VRT subclasses, unknown VRT band
# children, rotated source transforms on a VRT mosaic).
'UnsupportedGeoTIFFFeatureError',
# Issue #2443 (epic #2342): typed rejection when a caller
# opens a VRT under ``stable_only=True``. The VRT reader
# itself is advanced-tier so the request cannot be served
# without naming the broader-tier opt-in.
'VRTStableSourcesOnlyError',
'GeoTIFFFallbackWarning',
'UnsafeURLError',
# Canonical georef_status constants (issue #2136). Exposed
Expand Down
6 changes: 6 additions & 0 deletions xrspatial/geotiff/tests/test_reader_kwarg_order_1935.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
# opt-outs so the canonical order keeps the typed-error gates
# grouped.
"allow_invalid_nodata",
# Issue #2443 (epic #2342) added the stable-tier-only read-side
# gate. Sits alongside the other ambiguous-metadata opt-outs and
# immediately before the experimental-codec unlock it pairs with
# in the rejection message, so the canonical order tracks the
# release-contract grouping.
"stable_only",
# PR 4 of epic #2340 added the experimental / internal-only codec
# opt-ins on the read side, mirroring the writer surface from #2137
# / #1845. They sit after the other ``allow_*`` flags so the
Expand Down
Loading
Loading