Skip to content

Commit d1bd260

Browse files
authored
geotiff: deprecate matplotlib colormap variants in DataArray.attrs (#1984) (#2014)
* geotiff: deprecate matplotlib colormap variants in DataArray.attrs (#1984) Issue #1984 PR 7. The reader emits ``attrs['cmap']`` (when matplotlib is importable) and ``attrs['colormap_rgba']`` whenever the source file declares Photometric=3 (palette). The writer never selects Photometric=3 from attrs alone, so PR 6 (#2004) locked the round-trip drop for both keys as part of the attrs-contract pass-through tier. This PR deprecates the read-side emission of both keys for one release cycle. Callers who want a matplotlib ListedColormap should build one from the canonical ``attrs['colormap']`` (raw uint16 RGB triples from TIFF tag 320) instead. ``attrs['colormap']`` is the canonical-tier replacement and is intentionally kept: it round-trips through ``_merge_friendly_extra_tags`` already. During the deprecation window both attrs still land on the returned DataArray; only a DeprecationWarning is added. Removal is a follow-up PR. The new attrs-contract docstring split moves both keys into a "Deprecated" section so the contract stays explicit about which keys have a future. The new test file ``xrspatial/geotiff/tests/test_attrs_pr7_deprecate_colormap_variants_1984.py`` pins three behaviours: the warning fires on a Photometric=3 fixture for ``colormap_rgba`` (always) and for ``cmap`` (when matplotlib is installed); the attrs are still emitted; the plain ``attrs['colormap']`` key is unaffected. Existing tests that read palette fixtures (the TestPalette block in ``test_features.py`` and ``test_colormap_round_trip`` in ``test_metadata_round_trip_1484.py``) ignore the new DeprecationWarning locally to keep their reports clean. * geotiff: factor shared _emit_deprecated_geokey_attr helper PR review on #2014 flagged that the three colormap deprecation sites copy-paste the same nine-line ``warnings.warn(...)`` block with only the attr name and migration hint varying. Sibling PRs #2010 / #2013 have the same shape; PR #2011 already factors a slice-specific helper. Introduce a generic helper in ``_attrs.py`` so all four PR 7 slices can share it. ``_emit_deprecated_geokey_attr(attrs, name, value, *, reason, migration=None)`` builds the canonical warning text shape: xrspatial.geotiff: attrs['<name>'] is deprecated; <reason>. [<migration>.] It will be removed in a future release. See issue #1984. then sets ``attrs[name] = value`` (read-side emission still alive for the deprecation window). ``stacklevel=2`` lands the warning at the caller of ``_populate_attrs_from_geo_info``; ``DeprecationWarning`` is silenced for library code by default in Python's filters, so this is mainly visible to test runners and developers who opt in. The colormap-variants deprecation now routes ``cmap`` and the two ``colormap_rgba`` branches (matplotlib present + ImportError fallback) through the helper, replacing 27 lines of copy-paste with three calls. The emitted warning text is byte-identical to the previous inline strings, so the existing tests (``pytest.warns`` plus the TestPalette / test_metadata_round_trip filter regexes) keep passing unchanged. Sibling PRs #2010 and #2013 can adopt the helper on rebase; #2011's existing geographic-specific helper can be inlined onto this one in a follow-up. Test plan: - pytest xrspatial/geotiff/tests/test_attrs_pr7_deprecate_colormap_variants_1984.py -- 4 passed. - pytest xrspatial/geotiff/tests/test_attrs_contract_*_1984.py xrspatial/geotiff/tests/test_metadata_round_trip_1484.py -- 57 passed. - pytest xrspatial/geotiff/tests/test_features.py::TestPalette -- 7 passed. * geotiff: address self-review on shared deprecation helper Self-review on PR #2014 flagged one blocker and two suggestions on the shared-helper refactor. This commit addresses them. Blocker -- ``stacklevel`` regression (``_attrs.py:179``): Wrapping the inline ``warnings.warn(..., stacklevel=2)`` call in a helper shifted the warning location by one frame; the warning was surfacing at ``_attrs.py`` (the helper call site) instead of at the backend that called ``_populate_attrs_from_geo_info``. Bumped ``stacklevel=2`` to ``stacklevel=3`` so the warning is attributed to the backend frame, matching the pre-refactor behaviour (verified empirically: warning now surfaces at ``xrspatial/geotiff/__init__.py:518`` instead of ``_attrs.py:350``). Suggestion -- helper name (``_attrs.py:152``): Renamed ``_emit_deprecated_geokey_attr`` to ``_emit_deprecated_attr`` because the colormap variants (cmap, colormap_rgba) come from TIFF tag 320 (ColorMap), not from a GeoKey. The new name covers every PR 7 slice; docstring notes why the ``_geokey_`` qualifier was dropped. Suggestion -- user-guide doc: Added a new "Deprecated keys" section to ``docs/source/user_guide/attrs_contract.rst`` so users reading the contract page see the new tier alongside the existing canonical / compatibility-alias / pass-through tiers. Mirrors the module docstring. Suggestion -- direct unit test for the helper: Added ``test_emit_deprecated_attr_with_migration_text`` and ``test_emit_deprecated_attr_without_migration_text``. The first pins the exact warning text shape; the second pins the ``migration=None`` branch which the existing colormap tests do not exercise. Nit -- migration hints: Lifted ``_colormap_reason`` / ``_colormap_migration`` to module- level constants and split them into ``_DEPRECATED_COLORMAP_REASON``, ``_DEPRECATED_CMAP_MIGRATION``, and ``_DEPRECATED_COLORMAP_RGBA_MIGRATION``. The new ``colormap_rgba``-specific migration ("Reshape attrs['colormap'] to (n_colors, 3) and append an alpha channel") is more direct than the previous shared ``ListedColormap`` hint, which only matched ``cmap`` cleanly. Test plan: - pytest test_attrs_pr7_deprecate_colormap_variants_1984.py (6 passed) - pytest test_attrs_contract_*_1984.py test_metadata_round_trip_1484.py test_features.py::TestPalette -- 70 passed total
1 parent 20751b5 commit d1bd260

6 files changed

Lines changed: 483 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Refresh the geotiff mmap cache when a file is replaced under the same path so re-reads after an atomic-rename overwrite no longer return stale bytes
1212
- Decode TIFF predictor=3 un-transpose by file byte order so big-endian floating-point TIFFs read back exactly
1313
- Default internal `_vrt.read_vrt` `missing_sources` to `'raise'` so an unreadable VRT source no longer produces a silent zero-fill hole on integer rasters; pass `missing_sources='warn'` to opt back into the previous lenient behaviour (#1843)
14+
- Deprecate read-side emission of matplotlib colormap-derived attrs (cmap, colormap_rgba) on palette TIFFs; the writer cannot set Photometric=3 so they do not round-trip. Construct ListedColormap from attrs['colormap'] in caller code. These attrs still emit for now but trigger a DeprecationWarning. Removal planned for a future release. (#1984)
1415

1516

1617
### Version 0.9.9 - 2026-05-05

docs/source/user_guide/attrs_contract.rst

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,38 @@ must not assume a specific pass-through key survives a round-trip.
180180
* - ``colormap``
181181
- tuple
182182
- Raw ``ColorMap`` TIFF tag (tag id 320) values.
183+
184+
185+
Deprecated keys
186+
===============
187+
188+
These keys are still emitted on read for one release cycle, but each
189+
emission triggers a ``DeprecationWarning``. The writer cannot
190+
reconstruct them from canonical attrs, so they do not round-trip.
191+
Callers should migrate to the canonical alternative listed below
192+
before the warning-only window closes. See issue #1984.
193+
194+
.. list-table::
195+
:header-rows: 1
196+
:widths: 22 18 60
197+
198+
* - Key
199+
- Type
200+
- Definition and migration
183201
* - ``colormap_rgba``
184202
- array
185-
- Decoded RGBA colormap. Emitted only when the file's photometric
186-
interpretation is ``Photometric == 3`` (palette).
203+
- Decoded RGBA colormap. Emitted on read when the file's
204+
photometric interpretation is ``Photometric == 3`` (palette).
205+
The writer cannot set ``Photometric == 3`` so the attr does
206+
not round-trip. Reshape ``attrs['colormap']`` to
207+
``(n_colors, 3)`` and append an alpha channel in caller code
208+
if needed.
187209
* - ``cmap``
188210
- ``matplotlib.colors.ListedColormap``
189-
- Matplotlib colormap built from ``colormap_rgba``. Present only
190-
when matplotlib is importable and the same ``Photometric == 3``
191-
gate is satisfied.
211+
- Matplotlib colormap built from the palette. Same
212+
``Photometric == 3`` gate, same round-trip gap. Construct a
213+
``ListedColormap`` from ``attrs['colormap']`` in caller code
214+
if needed.
192215

193216

194217
Round-trip invariants

xrspatial/geotiff/_attrs.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@
6363
6464
- ``image_description``: TIFF ImageDescription tag.
6565
- ``extra_samples``: TIFF ExtraSamples tag.
66-
- ``colormap``, ``colormap_rgba``, ``cmap``: palette data attached to
67-
single-band paletted images.
66+
- ``colormap``: raw uint16 RGB triples from the TIFF ColorMap tag (320),
67+
attached to single-band paletted images.
6868
6969
Deprecated (will be removed in a future release; see issue #1984):
7070
@@ -103,6 +103,17 @@
103103
reason as ``vertical_crs``.
104104
- ``vertical_units``: VerticalUnitsGeoKey value. Same deprecation reason
105105
as ``vertical_crs``.
106+
Colormap variants (different root cause: photometric gate, not GeoKey):
107+
108+
- ``colormap_rgba``: RGBA palette array, only emitted on read when the
109+
source file is Photometric==3 (palette). The writer never selects
110+
Photometric=3, so this attr does not round-trip. Reshape
111+
``attrs['colormap']`` to ``(n_colors, 3)`` and append an alpha
112+
channel in caller code if needed.
113+
- ``cmap``: matplotlib ``ListedColormap`` built from the palette. Same
114+
Photometric==3 gate, same round-trip gap. Construct a
115+
``ListedColormap`` from ``attrs['colormap']`` in caller code if
116+
needed.
106117
107118
Migration recipe (the canonical replacement is ``crs`` / ``crs_wkt``
108119
plus a one-liner with :mod:`pyproj` when a derived value is needed)::
@@ -208,6 +219,25 @@
208219
)
209220

210221

222+
# Shared wording for the colormap-variants slice of the PR 7 deprecation
223+
# series. The root cause is a different one (the writer cannot set
224+
# ``Photometric=3``) so the GeoKey-tier reason templates above don't
225+
# fit; these constants are spliced into ``_emit_deprecated_attr`` with
226+
# a per-attr migration recipe so users see how to derive an RGBA palette
227+
# or matplotlib ``ListedColormap`` from canonical ``attrs['colormap']``.
228+
_DEPRECATED_COLORMAP_REASON = (
229+
"the writer cannot set Photometric=3 so it does not round-trip"
230+
)
231+
_DEPRECATED_CMAP_MIGRATION = (
232+
"Construct a ListedColormap from attrs['colormap'] in caller code "
233+
"if needed"
234+
)
235+
_DEPRECATED_COLORMAP_RGBA_MIGRATION = (
236+
"Reshape attrs['colormap'] to (n_colors, 3) and append an alpha "
237+
"channel in caller code if needed"
238+
)
239+
240+
211241
def _deprecated_geokey_warning(name: str, *, reason: str) -> str:
212242
"""Warning text for a deprecated GeoKey-derived attr.
213243
@@ -332,6 +362,54 @@ def _emit_deprecated_geographic_geokey(attrs: dict, name: str, value) -> None:
332362
)
333363

334364

365+
def _emit_deprecated_attr(
366+
attrs: dict,
367+
name: str,
368+
value,
369+
*,
370+
reason: str,
371+
migration: str | None = None,
372+
) -> None:
373+
"""Emit a deprecated attr with a ``DeprecationWarning`` and a
374+
per-attr migration recipe.
375+
376+
Sibling of :func:`_emit_deprecated_geokey_attr` that adds support
377+
for an optional ``migration`` clause spliced into the warning. The
378+
GeoKey-tier helper uses a fixed sentence ("... so it will not
379+
round-trip ..."); the colormap-variants tier needs to point users
380+
at how to derive an RGBA palette or matplotlib ``ListedColormap``
381+
from canonical ``attrs['colormap']``, which doesn't fit that
382+
template. A follow-up may unify the two helpers; for now they live
383+
side-by-side because the warning-text contracts are pinned by
384+
separate test suites and converging them is out of scope for the
385+
colormap slice.
386+
387+
Warning text shape::
388+
389+
xrspatial.geotiff: attrs['<name>'] is deprecated; <reason>
390+
<migration?> It will be removed in a future release. See
391+
issue #1984.
392+
393+
The ``stacklevel`` is taken from
394+
:func:`_stacklevel_to_external_caller` so the warning is attributed
395+
to the user's call site, matching the GeoKey-tier slices.
396+
"""
397+
parts = [
398+
f"xrspatial.geotiff: attrs[{name!r}] is deprecated;",
399+
reason.rstrip('.') + '.',
400+
]
401+
if migration:
402+
parts.append(migration.rstrip('.') + '.')
403+
parts.append("It will be removed in a future release. See issue #1984.")
404+
warnings.warn(
405+
' '.join(parts),
406+
DeprecationWarning,
407+
stacklevel=_stacklevel_to_external_caller(),
408+
)
409+
attrs[name] = value
410+
411+
412+
335413
def _extent_to_window(transform, file_height, file_width,
336414
y_min, y_max, x_min, x_max):
337415
"""Convert geographic extent to pixel window (row_start, col_start, row_stop, col_stop).
@@ -513,11 +591,23 @@ def _populate_attrs_from_geo_info(attrs: dict, geo_info, *, window=None) -> None
513591
if geo_info.colormap is not None:
514592
try:
515593
from matplotlib.colors import ListedColormap
516-
attrs['cmap'] = ListedColormap(
517-
geo_info.colormap, name='tiff_palette')
518-
attrs['colormap_rgba'] = geo_info.colormap
594+
_emit_deprecated_attr(
595+
attrs, 'cmap',
596+
ListedColormap(geo_info.colormap, name='tiff_palette'),
597+
reason=_DEPRECATED_COLORMAP_REASON,
598+
migration=_DEPRECATED_CMAP_MIGRATION,
599+
)
600+
_emit_deprecated_attr(
601+
attrs, 'colormap_rgba', geo_info.colormap,
602+
reason=_DEPRECATED_COLORMAP_REASON,
603+
migration=_DEPRECATED_COLORMAP_RGBA_MIGRATION,
604+
)
519605
except ImportError:
520-
attrs['colormap_rgba'] = geo_info.colormap
606+
_emit_deprecated_attr(
607+
attrs, 'colormap_rgba', geo_info.colormap,
608+
reason=_DEPRECATED_COLORMAP_REASON,
609+
migration=_DEPRECATED_COLORMAP_RGBA_MIGRATION,
610+
)
521611

522612
if geo_info.extra_tags is not None:
523613
for _tag_id, _tt, _tc, _tv in geo_info.extra_tags:

0 commit comments

Comments
 (0)