Summary
The three public writer entry points in xrspatial.geotiff disagree on the CRS kwarg name and on the accepted input forms:
def to_geotiff(data, path, *, crs: int | str | None = None, ...):
"""crs : int, str, or None
EPSG code (int), WKT string, or PROJ string. ...
"""
def write_geotiff_gpu(data, path, *, crs: int | str | None = None, ...):
"""crs : int, str, or None
EPSG code or WKT string.
"""
def write_vrt(vrt_path, source_files, *, relative=True,
crs_wkt: str | None = None, ...):
"""crs_wkt : str or None
CRS as a WKT string. If None, the CRS is taken from the first
source GeoTIFF.
"""
The reader entry points (open_geotiff, read_geotiff_dask, read_geotiff_gpu, read_vrt) surface the file's CRS as attrs['crs'] (an int EPSG when available) with the WKT fallback under attrs['crs_wkt']. So a round-trip pattern like
da = open_geotiff('in.tif')
to_geotiff(da, 'out.tif', crs=4326) # works
write_geotiff_gpu(da, 'out.tif', crs=4326) # works
write_vrt('out.vrt', [...], crs=4326) # TypeError: unexpected keyword 'crs'
forces the caller to remember which writer uses which kwarg and to convert EPSG int -> WKT manually for the VRT writer only. This is a real Cat 1 surface-drift finding: same concept (the output CRS), three writers, two distinct kwarg names.
The asymmetry also makes write_vrt impossible to call uniformly from generic write-wrapper code: code that forwards crs=<value> to whichever writer matches the output extension has to special-case VRT.
Severity
MEDIUM (Cat 1, parameter naming drift). Not a correctness bug; the workaround is one line of pyproj. But it surprises users and makes IDE autocomplete inconsistent across the writer trio.
Proposed fix
Add a new crs kwarg on write_vrt mirroring to_geotiff/write_geotiff_gpu. Keep crs_wkt for backward compatibility with a DeprecationWarning. Reject passing both.
def write_vrt(vrt_path, source_files, *, relative=True,
crs: int | str | None = None,
crs_wkt: str | None = None, # deprecated alias
nodata: float | int | None = None) -> str:
if crs is not None and crs_wkt is not None:
raise TypeError("write_vrt: pass either 'crs' or 'crs_wkt', not both")
if crs_wkt is not None:
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
# Resolve crs to a WKT string (or None) before forwarding
resolved_wkt = _resolve_crs_to_wkt(crs)
return _write_vrt_internal(
vrt_path, source_files,
relative=relative,
crs_wkt=resolved_wkt,
nodata=nodata,
)
_resolve_crs_to_wkt accepts:
None -> returns None (no override; downstream picks CRS from the first source).
int -> pyproj.CRS.from_epsg(<int>).to_wkt().
str -> if it looks like an EPSG/AUTHORITY code, hand to pyproj; otherwise pass through as already-WKT or PROJ string. The wrapper can keep _wkt_to_epsg as its parser by inverting it: send the string through pyproj, then call to_wkt() on the result so PROJ-string callers get a normalized WKT.
Test plan:
- Pin
inspect.signature(write_vrt) exposes both crs and crs_wkt.
write_vrt(..., crs=4326) writes a VRT whose <SRS> element contains EPSG:4326's WKT.
write_vrt(..., crs_wkt='GEOGCS[...]') still works but emits DeprecationWarning.
- Passing both
crs= and crs_wkt= raises TypeError.
- Read-back round trip:
read_vrt(out_vrt).attrs['crs'] == 4326.
Non-breaking (deprecation shim; crs_wkt still works).
Discovered by
/sweep-api-consistency against the geotiff module on 2026-05-12.
Summary
The three public writer entry points in
xrspatial.geotiffdisagree on the CRS kwarg name and on the accepted input forms:The reader entry points (
open_geotiff,read_geotiff_dask,read_geotiff_gpu,read_vrt) surface the file's CRS asattrs['crs'](an int EPSG when available) with the WKT fallback underattrs['crs_wkt']. So a round-trip pattern likeforces the caller to remember which writer uses which kwarg and to convert EPSG int -> WKT manually for the VRT writer only. This is a real Cat 1 surface-drift finding: same concept (the output CRS), three writers, two distinct kwarg names.
The asymmetry also makes
write_vrtimpossible to call uniformly from generic write-wrapper code: code that forwardscrs=<value>to whichever writer matches the output extension has to special-case VRT.Severity
MEDIUM (Cat 1, parameter naming drift). Not a correctness bug; the workaround is one line of pyproj. But it surprises users and makes IDE autocomplete inconsistent across the writer trio.
Proposed fix
Add a new
crskwarg onwrite_vrtmirroringto_geotiff/write_geotiff_gpu. Keepcrs_wktfor backward compatibility with aDeprecationWarning. Reject passing both._resolve_crs_to_wktaccepts:None-> returnsNone(no override; downstream picks CRS from the first source).int->pyproj.CRS.from_epsg(<int>).to_wkt().str-> if it looks like an EPSG/AUTHORITY code, hand to pyproj; otherwise pass through as already-WKT or PROJ string. The wrapper can keep_wkt_to_epsgas its parser by inverting it: send the string through pyproj, then callto_wkt()on the result so PROJ-string callers get a normalized WKT.Test plan:
inspect.signature(write_vrt)exposes bothcrsandcrs_wkt.write_vrt(..., crs=4326)writes a VRT whose<SRS>element contains EPSG:4326's WKT.write_vrt(..., crs_wkt='GEOGCS[...]')still works but emitsDeprecationWarning.crs=andcrs_wkt=raisesTypeError.read_vrt(out_vrt).attrs['crs'] == 4326.Non-breaking (deprecation shim;
crs_wktstill works).Discovered by
/sweep-api-consistencyagainst thegeotiffmodule on 2026-05-12.