Skip to content

Commit

Permalink
Merge pull request matplotlib#16991 from greglucas/get_cmap_warn
Browse files Browse the repository at this point in the history
Begin warning on modifying global state of colormaps
  • Loading branch information
timhoffm authored Apr 27, 2020
2 parents b94812c + 692b83c commit 50ef5d9
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 16 deletions.
2 changes: 1 addition & 1 deletion examples/images_contours_and_fields/demo_bboximage.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
a = np.vstack((a, a))

# List of all colormaps; skip reversed colormaps.
maps = sorted(m for m in plt.cm.cmap_d if not m.endswith("_r"))
maps = sorted(m for m in plt.colormaps() if not m.endswith("_r"))

ncol = 2
nrow = len(maps)//ncol + 1
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/qt_editor/figureoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ def prepare_data(d, init):
mappabledict[label] = mappable
mappablelabels = sorted(mappabledict, key=cmp_key)
mappables = []
cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())]
cmaps = [(cmap, name) for name, cmap in sorted(cm._cmap_registry.items())]
for label in mappablelabels:
mappable = mappabledict[label]
cmap = mappable.get_cmap()
if cmap not in cm.cmap_d.values():
if cmap not in cm._cmap_registry.values():
cmaps = [(cmap, cmap.name), *cmaps]
low, high = mappable.get_clim()
mappabledata = [
Expand Down
80 changes: 70 additions & 10 deletions lib/matplotlib/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
normalization.
"""

from collections.abc import MutableMapping
import functools

import numpy as np
Expand Down Expand Up @@ -49,7 +50,7 @@ def revcmap(data):
LUTSIZE = mpl.rcParams['image.lut']


def _gen_cmap_d():
def _gen_cmap_registry():
"""
Generate a dict mapping standard colormap names to standard colormaps, as
well as the reversed colormaps.
Expand All @@ -65,12 +66,56 @@ def _gen_cmap_d():
# Generate reversed cmaps.
for cmap in list(cmap_d.values()):
rmap = cmap.reversed()
cmap._global = True
rmap._global = True
cmap_d[rmap.name] = rmap
return cmap_d


cmap_d = _gen_cmap_d()
locals().update(cmap_d)
class _DeprecatedCmapDictWrapper(MutableMapping):
"""Dictionary mapping for deprecated _cmap_d access."""

def __init__(self, cmap_registry):
self._cmap_registry = cmap_registry

def __delitem__(self, key):
self._warn_deprecated()
self._cmap_registry.__delitem__(key)

def __getitem__(self, key):
self._warn_deprecated()
return self._cmap_registry.__getitem__(key)

def __iter__(self):
self._warn_deprecated()
return self._cmap_registry.__iter__()

def __len__(self):
self._warn_deprecated()
return self._cmap_registry.__len__()

def __setitem__(self, key, val):
self._warn_deprecated()
self._cmap_registry.__setitem__(key, val)

def get(self, key, default=None):
self._warn_deprecated()
return self._cmap_registry.get(key, default)

def _warn_deprecated(self):
cbook.warn_deprecated(
"3.3",
message="The global colormaps dictionary is no longer "
"considered public API.",
alternative="Please use register_cmap() and get_cmap() to "
"access the contents of the dictionary."
)


_cmap_registry = _gen_cmap_registry()
locals().update(_cmap_registry)
# This is no longer considered public API
cmap_d = _DeprecatedCmapDictWrapper(_cmap_registry)


# Continue with definitions ...
Expand All @@ -95,6 +140,13 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
and the resulting colormap is registered. Instead of this implicit
colormap creation, create a `.LinearSegmentedColormap` and use the first
case: ``register_cmap(cmap=LinearSegmentedColormap(name, data, lut))``.
Notes
-----
Registering a colormap stores a reference to the colormap object
which can currently be modified and inadvertantly change the global
colormap state. This behavior is deprecated and in Matplotlib 3.5
the registered colormap will be immutable.
"""
cbook._check_isinstance((str, None), name=name)
if name is None:
Expand All @@ -104,7 +156,8 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
raise ValueError("Arguments must include a name or a "
"Colormap") from err
if isinstance(cmap, colors.Colormap):
cmap_d[name] = cmap
cmap._global = True
_cmap_registry[name] = cmap
return
if lut is not None or data is not None:
cbook.warn_deprecated(
Expand All @@ -117,7 +170,8 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
if lut is None:
lut = mpl.rcParams['image.lut']
cmap = colors.LinearSegmentedColormap(name, data, lut)
cmap_d[name] = cmap
cmap._global = True
_cmap_registry[name] = cmap


def get_cmap(name=None, lut=None):
Expand All @@ -127,11 +181,17 @@ def get_cmap(name=None, lut=None):
Colormaps added with :func:`register_cmap` take precedence over
built-in colormaps.
Notes
-----
Currently, this returns the global colormap object, which is deprecated.
In Matplotlib 3.5, you will no longer be able to modify the global
colormaps in-place.
Parameters
----------
name : `matplotlib.colors.Colormap` or str or None, default: None
If a `.Colormap` instance, it will be returned. Otherwise, the name of
a colormap known to Matplotlib, which will be resampled by *lut*. The
If a `.Colormap` instance, it will be returned. Otherwise, the name of
a colormap known to Matplotlib, which will be resampled by *lut*. The
default, None, means :rc:`image.cmap`.
lut : int or None, default: None
If *name* is not already a Colormap instance and *lut* is not None, the
Expand All @@ -141,11 +201,11 @@ def get_cmap(name=None, lut=None):
name = mpl.rcParams['image.cmap']
if isinstance(name, colors.Colormap):
return name
cbook._check_in_list(sorted(cmap_d), name=name)
cbook._check_in_list(sorted(_cmap_registry), name=name)
if lut is None:
return cmap_d[name]
return _cmap_registry[name]
else:
return cmap_d[name]._resample(lut)
return _cmap_registry[name]._resample(lut)


class ScalarMappable:
Expand Down
16 changes: 16 additions & 0 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,18 @@ def makeMappingArray(N, data, gamma=1.0):
return _create_lookup_table(N, data, gamma)


def _warn_if_global_cmap_modified(cmap):
if getattr(cmap, '_global', False):
cbook.warn_deprecated(
"3.3",
message="You are modifying the state of a globally registered "
"colormap. In future versions, you will not be able to "
"modify a registered colormap in-place. To remove this "
"warning, you can make a copy of the colormap first. "
f"cmap = mpl.cm.get_cmap({cmap.name}).copy()"
)


class Colormap:
"""
Baseclass for all scalar to RGBA mappings.
Expand Down Expand Up @@ -599,10 +611,12 @@ def __copy__(self):
cmapobject.__dict__.update(self.__dict__)
if self._isinit:
cmapobject._lut = np.copy(self._lut)
cmapobject._global = False
return cmapobject

def set_bad(self, color='k', alpha=None):
"""Set the color for masked values."""
_warn_if_global_cmap_modified(self)
self._rgba_bad = to_rgba(color, alpha)
if self._isinit:
self._set_extremes()
Expand All @@ -611,6 +625,7 @@ def set_under(self, color='k', alpha=None):
"""
Set the color for low out-of-range values when ``norm.clip = False``.
"""
_warn_if_global_cmap_modified(self)
self._rgba_under = to_rgba(color, alpha)
if self._isinit:
self._set_extremes()
Expand All @@ -619,6 +634,7 @@ def set_over(self, color='k', alpha=None):
"""
Set the color for high out-of-range values when ``norm.clip = False``.
"""
_warn_if_global_cmap_modified(self)
self._rgba_over = to_rgba(color, alpha)
if self._isinit:
self._set_extremes()
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1948,7 +1948,7 @@ def colormaps():
<https://www.mathworks.com/matlabcentral/fileexchange/2662-cmrmap-m>`_
by Carey Rappaport
"""
return sorted(cm.cmap_d)
return sorted(cm._cmap_registry)


def _setup_pyplot_info_docstrings():
Expand Down
37 changes: 36 additions & 1 deletion lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,41 @@ def test_register_cmap():
cm.register_cmap()


def test_colormap_global_set_warn():
new_cm = plt.get_cmap('viridis')
# Store the old value so we don't override the state later on.
orig_cmap = copy.copy(new_cm)
with pytest.warns(cbook.MatplotlibDeprecationWarning,
match="You are modifying the state of a globally"):
# This should warn now because we've modified the global state
new_cm.set_under('k')

# This shouldn't warn because it is a copy
copy.copy(new_cm).set_under('b')

# Test that registering and then modifying warns
plt.register_cmap(name='test_cm', cmap=copy.copy(orig_cmap))
new_cm = plt.get_cmap('test_cm')
with pytest.warns(cbook.MatplotlibDeprecationWarning,
match="You are modifying the state of a globally"):
# This should warn now because we've modified the global state
new_cm.set_under('k')

# Re-register the original
plt.register_cmap(cmap=orig_cmap)


def test_colormap_dict_deprecate():
# Make sure we warn on get and set access into cmap_d
with pytest.warns(cbook.MatplotlibDeprecationWarning,
match="The global colormaps dictionary is no longer"):
cm = plt.cm.cmap_d['viridis']

with pytest.warns(cbook.MatplotlibDeprecationWarning,
match="The global colormaps dictionary is no longer"):
plt.cm.cmap_d['test'] = cm


def test_colormap_copy():
cm = plt.cm.Reds
cm_copy = copy.copy(cm)
Expand Down Expand Up @@ -818,7 +853,7 @@ def test_pandas_iterable(pd):
assert_array_equal(cm1.colors, cm2.colors)


@pytest.mark.parametrize('name', sorted(cm.cmap_d))
@pytest.mark.parametrize('name', sorted(plt.colormaps()))
def test_colormap_reversing(name):
"""
Check the generated _lut data of a colormap and corresponding reversed
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def test_shared():
assert fig.axes[1].get_xlim() == (10, 20)


@pytest.mark.parametrize("cmap", cm.cmap_d.values())
@pytest.mark.parametrize("cmap", cm._cmap_registry.values())
def test_cmap(cmap):
pickle.dumps(cmap)

Expand Down

0 comments on commit 50ef5d9

Please sign in to comment.