"""VRT writer entry point.
Wraps ``_vrt.write_vrt`` with the public ``write_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``.
"""
from __future__ import annotations
import warnings
from .._crs import _resolve_crs_to_wkt
from .._runtime import (_CRS_WKT_DEPRECATED_SENTINEL, _VRT_PATH_DEPRECATED_SENTINEL,
_VRT_PATH_MISSING_SENTINEL)
from .._validation import _validate_nodata_arg
[docs]
def write_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.
Output targets the same narrow subset of GDAL's VRT spec that the
reader supports (see the "VRT support matrix" section in
``docs/source/reference/geotiff.rst`` and the audited matrix in
``docs/source/reference/release_gate_geotiff.rst`` for the
canonical contract):
* Supported: simple GDAL VRT mosaics over GeoTIFF sources;
compatible CRS, transform orientation, pixel size, dtype, and
band count across sources; clean windowed reads on the
consumer side; lazy / dask reads over the same subset on the
consumer side; explicit nodata; ``missing_sources='raise'`` as
the read-side default.
* Non-goals (the writer does not emit these and the reader is
allowed to raise on them): warped / reprojection VRTs,
arbitrary resampling beyond the tested subset, mixed CRS /
resolution / dtype / band metadata without an opt-in, nested
VRTs, complex source / mask band / alpha band structures, full
GDAL VRT parity.
Parameters
----------
path : str
[advanced] 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.
vrt_path : str, optional
[internal-only] Deprecated alias for ``path``. Emits
``DeprecationWarning`` when supplied; passing both ``path``
and ``vrt_path`` raises ``TypeError``. Kept so existing
callers (``write_vrt(vrt_path, sources)`` positional or
``write_vrt(vrt_path=...)`` keyword) keep working through the
deprecation window. New code should use ``path``.
relative : bool, optional
[advanced] 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
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
caller picked without per-writer special-casing.
crs_wkt : str or None, optional
[internal-only] Deprecated alias for ``crs``. Emits
``DeprecationWarning`` when supplied (including
``crs_wkt=None``); passing both ``crs`` and ``crs_wkt`` raises
``TypeError``. The value is forwarded through the same
``_resolve_crs_to_wkt`` path as ``crs``, so any string the
resolver accepts (WKT root keyword, PROJ string,
``"EPSG:NNNN"``) and ``None`` work here. The historic
``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
GeoTIFF.
Integer sentinels (e.g. ``65535`` for uint16, ``-9999`` for
int32) are accepted so the surface lines up with the
``nodata`` kwarg on ``to_geotiff`` and ``write_geotiff_gpu``.
Returns
-------
str
Path to the written VRT file.
Examples
--------
Safe usage. Mosaic two compatible tiles; the consumer can then
read the resulting VRT with the fail-closed defaults. Paths
below are illustrative; replace with paths to real GeoTIFF
files on disk:
>>> from xrspatial.geotiff import write_vrt, open_geotiff
>>> vrt_path = write_vrt( # doctest: +SKIP
... 'mosaic.vrt',
... source_files=['tile_west.tif', 'tile_east.tif'],
... )
>>> da = open_geotiff(vrt_path) # doctest: +SKIP
Intentionally raises (on the read side). If the source tiles
disagree on their per-band nodata sentinels, the default
``band_nodata=None`` on ``open_geotiff`` / ``read_vrt`` rejects
the mosaic with ``MixedBandMetadataError``. The writer does not
pre-validate cross-tile metadata; the failure mode lives on the
read side:
>>> from xrspatial.geotiff import MixedBandMetadataError
>>> # tile_a.tif declares nodata=-9999; tile_b.tif declares nodata=0
>>> bad_path = write_vrt( # doctest: +SKIP
... 'mixed_nodata.vrt',
... source_files=['tile_a.tif', 'tile_b.tif'],
... )
>>> try: # doctest: +SKIP
... open_geotiff(bad_path)
... except MixedBandMetadataError:
... pass # fix the source tiles or pass band_nodata='first'.
"""
# Explicit signature (previously ``**kwargs``) so ``inspect.signature``,
# IDE autocomplete, and ``mypy --strict`` can see the accepted kwargs
# without parsing the docstring. Mirrors ``_vrt.write_vrt`` for the
# historic ``crs_wkt`` path; the new ``crs`` path normalises through
# ``_resolve_crs_to_wkt`` before forwarding because the internal
# writer still only speaks WKT.
#
# The ``path`` / ``vrt_path`` shim resolves the destination kwarg
# before any other processing so the rest of the function works
# uniformly against a single ``vrt_path`` local. ``path`` is the
# new name (parity with to_geotiff / write_geotiff_gpu); ``vrt_path``
# is kept as a deprecated alias to preserve back-compat for callers
# using either positional ``write_vrt(vrt_path, sources)`` or
# keyword ``write_vrt(vrt_path=...)``.
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:
# Both supplied is ambiguous regardless of whether the two values
# happen to be the same string. Refuse rather than silently
# picking one. Mirrors the same rule the ``crs`` / ``crs_wkt``
# shim below applies.
raise TypeError(
"write_vrt: pass either 'path' or the deprecated 'vrt_path' "
"alias, not both.")
if vrt_path_passed:
warnings.warn(
"write_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.",
DeprecationWarning,
stacklevel=2,
)
path = vrt_path
elif not path_passed:
# Neither name supplied. Match the previous ``TypeError: missing
# required positional argument`` semantics by raising rather than
# forwarding the sentinel into ``_write_vrt_internal``.
raise TypeError(
"write_vrt: missing required argument 'path'")
if path is None:
# Explicit ``path=None`` (including positional ``write_vrt(None,
# sources)``) is rejected up front so the error message names the
# offending kwarg instead of crashing deep in
# ``os.path.dirname(os.path.abspath(None))``. The sentinel default
# on ``path`` is what lets us distinguish this case from "caller
# passed nothing" above.
raise TypeError(
"write_vrt: 'path' must be a str, got None")
if source_files is None:
raise TypeError(
"write_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:
# Both supplied is ambiguous regardless of whether the WKT happens
# to encode the same CRS as the int. Refuse rather than silently
# picking one.
raise TypeError(
"write_vrt: pass either 'crs' or the deprecated 'crs_wkt' "
"alias, not both.")
if crs_wkt_passed:
warnings.warn(
"write_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.",
DeprecationWarning,
stacklevel=2,
)
crs = crs_wkt
# Reject bool / non-numeric nodata at the entry point so write_vrt
# matches the to_geotiff / write_geotiff_gpu surface. ``bool`` is a
# subclass of ``int`` in Python, so a typo like ``nodata=True`` would
# slip past every downstream ``isinstance(nodata, (int, float))``
# guard and the VRT XML emitter would write ``<NoDataValue>True
# </NoDataValue>``. No reader parses ``"True"`` as numeric, so the
# round-trip would silently drop the sentinel.
_validate_nodata_arg(nodata)
resolved_wkt = _resolve_crs_to_wkt(crs)
from .._vrt import write_vrt as _write_vrt_internal
return _write_vrt_internal(
path, source_files,
relative=relative,
crs_wkt=resolved_wkt,
nodata=nodata,
)