Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 50ef5d9

Browse files
authored
Merge pull request #16991 from greglucas/get_cmap_warn
Begin warning on modifying global state of colormaps
2 parents b94812c + 692b83c commit 50ef5d9

File tree

7 files changed

+127
-16
lines changed

7 files changed

+127
-16
lines changed

examples/images_contours_and_fields/demo_bboximage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
a = np.vstack((a, a))
3939

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

4343
ncol = 2
4444
nrow = len(maps)//ncol + 1

lib/matplotlib/backends/qt_editor/figureoptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,11 @@ def prepare_data(d, init):
140140
mappabledict[label] = mappable
141141
mappablelabels = sorted(mappabledict, key=cmp_key)
142142
mappables = []
143-
cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())]
143+
cmaps = [(cmap, name) for name, cmap in sorted(cm._cmap_registry.items())]
144144
for label in mappablelabels:
145145
mappable = mappabledict[label]
146146
cmap = mappable.get_cmap()
147-
if cmap not in cm.cmap_d.values():
147+
if cmap not in cm._cmap_registry.values():
148148
cmaps = [(cmap, cmap.name), *cmaps]
149149
low, high = mappable.get_clim()
150150
mappabledata = [

lib/matplotlib/cm.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
normalization.
1616
"""
1717

18+
from collections.abc import MutableMapping
1819
import functools
1920

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

5152

52-
def _gen_cmap_d():
53+
def _gen_cmap_registry():
5354
"""
5455
Generate a dict mapping standard colormap names to standard colormaps, as
5556
well as the reversed colormaps.
@@ -65,12 +66,56 @@ def _gen_cmap_d():
6566
# Generate reversed cmaps.
6667
for cmap in list(cmap_d.values()):
6768
rmap = cmap.reversed()
69+
cmap._global = True
70+
rmap._global = True
6871
cmap_d[rmap.name] = rmap
6972
return cmap_d
7073

7174

72-
cmap_d = _gen_cmap_d()
73-
locals().update(cmap_d)
75+
class _DeprecatedCmapDictWrapper(MutableMapping):
76+
"""Dictionary mapping for deprecated _cmap_d access."""
77+
78+
def __init__(self, cmap_registry):
79+
self._cmap_registry = cmap_registry
80+
81+
def __delitem__(self, key):
82+
self._warn_deprecated()
83+
self._cmap_registry.__delitem__(key)
84+
85+
def __getitem__(self, key):
86+
self._warn_deprecated()
87+
return self._cmap_registry.__getitem__(key)
88+
89+
def __iter__(self):
90+
self._warn_deprecated()
91+
return self._cmap_registry.__iter__()
92+
93+
def __len__(self):
94+
self._warn_deprecated()
95+
return self._cmap_registry.__len__()
96+
97+
def __setitem__(self, key, val):
98+
self._warn_deprecated()
99+
self._cmap_registry.__setitem__(key, val)
100+
101+
def get(self, key, default=None):
102+
self._warn_deprecated()
103+
return self._cmap_registry.get(key, default)
104+
105+
def _warn_deprecated(self):
106+
cbook.warn_deprecated(
107+
"3.3",
108+
message="The global colormaps dictionary is no longer "
109+
"considered public API.",
110+
alternative="Please use register_cmap() and get_cmap() to "
111+
"access the contents of the dictionary."
112+
)
113+
114+
115+
_cmap_registry = _gen_cmap_registry()
116+
locals().update(_cmap_registry)
117+
# This is no longer considered public API
118+
cmap_d = _DeprecatedCmapDictWrapper(_cmap_registry)
74119

75120

76121
# Continue with definitions ...
@@ -95,6 +140,13 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
95140
and the resulting colormap is registered. Instead of this implicit
96141
colormap creation, create a `.LinearSegmentedColormap` and use the first
97142
case: ``register_cmap(cmap=LinearSegmentedColormap(name, data, lut))``.
143+
144+
Notes
145+
-----
146+
Registering a colormap stores a reference to the colormap object
147+
which can currently be modified and inadvertantly change the global
148+
colormap state. This behavior is deprecated and in Matplotlib 3.5
149+
the registered colormap will be immutable.
98150
"""
99151
cbook._check_isinstance((str, None), name=name)
100152
if name is None:
@@ -104,7 +156,8 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
104156
raise ValueError("Arguments must include a name or a "
105157
"Colormap") from err
106158
if isinstance(cmap, colors.Colormap):
107-
cmap_d[name] = cmap
159+
cmap._global = True
160+
_cmap_registry[name] = cmap
108161
return
109162
if lut is not None or data is not None:
110163
cbook.warn_deprecated(
@@ -117,7 +170,8 @@ def register_cmap(name=None, cmap=None, data=None, lut=None):
117170
if lut is None:
118171
lut = mpl.rcParams['image.lut']
119172
cmap = colors.LinearSegmentedColormap(name, data, lut)
120-
cmap_d[name] = cmap
173+
cmap._global = True
174+
_cmap_registry[name] = cmap
121175

122176

123177
def get_cmap(name=None, lut=None):
@@ -127,11 +181,17 @@ def get_cmap(name=None, lut=None):
127181
Colormaps added with :func:`register_cmap` take precedence over
128182
built-in colormaps.
129183
184+
Notes
185+
-----
186+
Currently, this returns the global colormap object, which is deprecated.
187+
In Matplotlib 3.5, you will no longer be able to modify the global
188+
colormaps in-place.
189+
130190
Parameters
131191
----------
132192
name : `matplotlib.colors.Colormap` or str or None, default: None
133-
If a `.Colormap` instance, it will be returned. Otherwise, the name of
134-
a colormap known to Matplotlib, which will be resampled by *lut*. The
193+
If a `.Colormap` instance, it will be returned. Otherwise, the name of
194+
a colormap known to Matplotlib, which will be resampled by *lut*. The
135195
default, None, means :rc:`image.cmap`.
136196
lut : int or None, default: None
137197
If *name* is not already a Colormap instance and *lut* is not None, the
@@ -141,11 +201,11 @@ def get_cmap(name=None, lut=None):
141201
name = mpl.rcParams['image.cmap']
142202
if isinstance(name, colors.Colormap):
143203
return name
144-
cbook._check_in_list(sorted(cmap_d), name=name)
204+
cbook._check_in_list(sorted(_cmap_registry), name=name)
145205
if lut is None:
146-
return cmap_d[name]
206+
return _cmap_registry[name]
147207
else:
148-
return cmap_d[name]._resample(lut)
208+
return _cmap_registry[name]._resample(lut)
149209

150210

151211
class ScalarMappable:

lib/matplotlib/colors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,18 @@ def makeMappingArray(N, data, gamma=1.0):
484484
return _create_lookup_table(N, data, gamma)
485485

486486

487+
def _warn_if_global_cmap_modified(cmap):
488+
if getattr(cmap, '_global', False):
489+
cbook.warn_deprecated(
490+
"3.3",
491+
message="You are modifying the state of a globally registered "
492+
"colormap. In future versions, you will not be able to "
493+
"modify a registered colormap in-place. To remove this "
494+
"warning, you can make a copy of the colormap first. "
495+
f"cmap = mpl.cm.get_cmap({cmap.name}).copy()"
496+
)
497+
498+
487499
class Colormap:
488500
"""
489501
Baseclass for all scalar to RGBA mappings.
@@ -599,10 +611,12 @@ def __copy__(self):
599611
cmapobject.__dict__.update(self.__dict__)
600612
if self._isinit:
601613
cmapobject._lut = np.copy(self._lut)
614+
cmapobject._global = False
602615
return cmapobject
603616

604617
def set_bad(self, color='k', alpha=None):
605618
"""Set the color for masked values."""
619+
_warn_if_global_cmap_modified(self)
606620
self._rgba_bad = to_rgba(color, alpha)
607621
if self._isinit:
608622
self._set_extremes()
@@ -611,6 +625,7 @@ def set_under(self, color='k', alpha=None):
611625
"""
612626
Set the color for low out-of-range values when ``norm.clip = False``.
613627
"""
628+
_warn_if_global_cmap_modified(self)
614629
self._rgba_under = to_rgba(color, alpha)
615630
if self._isinit:
616631
self._set_extremes()
@@ -619,6 +634,7 @@ def set_over(self, color='k', alpha=None):
619634
"""
620635
Set the color for high out-of-range values when ``norm.clip = False``.
621636
"""
637+
_warn_if_global_cmap_modified(self)
622638
self._rgba_over = to_rgba(color, alpha)
623639
if self._isinit:
624640
self._set_extremes()

lib/matplotlib/pyplot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1948,7 +1948,7 @@ def colormaps():
19481948
<https://www.mathworks.com/matlabcentral/fileexchange/2662-cmrmap-m>`_
19491949
by Carey Rappaport
19501950
"""
1951-
return sorted(cm.cmap_d)
1951+
return sorted(cm._cmap_registry)
19521952

19531953

19541954
def _setup_pyplot_info_docstrings():

lib/matplotlib/tests/test_colors.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@ def test_register_cmap():
7070
cm.register_cmap()
7171

7272

73+
def test_colormap_global_set_warn():
74+
new_cm = plt.get_cmap('viridis')
75+
# Store the old value so we don't override the state later on.
76+
orig_cmap = copy.copy(new_cm)
77+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
78+
match="You are modifying the state of a globally"):
79+
# This should warn now because we've modified the global state
80+
new_cm.set_under('k')
81+
82+
# This shouldn't warn because it is a copy
83+
copy.copy(new_cm).set_under('b')
84+
85+
# Test that registering and then modifying warns
86+
plt.register_cmap(name='test_cm', cmap=copy.copy(orig_cmap))
87+
new_cm = plt.get_cmap('test_cm')
88+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
89+
match="You are modifying the state of a globally"):
90+
# This should warn now because we've modified the global state
91+
new_cm.set_under('k')
92+
93+
# Re-register the original
94+
plt.register_cmap(cmap=orig_cmap)
95+
96+
97+
def test_colormap_dict_deprecate():
98+
# Make sure we warn on get and set access into cmap_d
99+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
100+
match="The global colormaps dictionary is no longer"):
101+
cm = plt.cm.cmap_d['viridis']
102+
103+
with pytest.warns(cbook.MatplotlibDeprecationWarning,
104+
match="The global colormaps dictionary is no longer"):
105+
plt.cm.cmap_d['test'] = cm
106+
107+
73108
def test_colormap_copy():
74109
cm = plt.cm.Reds
75110
cm_copy = copy.copy(cm)
@@ -818,7 +853,7 @@ def test_pandas_iterable(pd):
818853
assert_array_equal(cm1.colors, cm2.colors)
819854

820855

821-
@pytest.mark.parametrize('name', sorted(cm.cmap_d))
856+
@pytest.mark.parametrize('name', sorted(plt.colormaps()))
822857
def test_colormap_reversing(name):
823858
"""
824859
Check the generated _lut data of a colormap and corresponding reversed

lib/matplotlib/tests/test_pickle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def test_shared():
190190
assert fig.axes[1].get_xlim() == (10, 20)
191191

192192

193-
@pytest.mark.parametrize("cmap", cm.cmap_d.values())
193+
@pytest.mark.parametrize("cmap", cm._cmap_registry.values())
194194
def test_cmap(cmap):
195195
pickle.dumps(cmap)
196196

0 commit comments

Comments
 (0)