diff --git a/doc/api/next_api_changes/behavior/15127-TAC.rst b/doc/api/next_api_changes/behavior/15127-TAC.rst new file mode 100644 index 000000000000..fd68c0150551 --- /dev/null +++ b/doc/api/next_api_changes/behavior/15127-TAC.rst @@ -0,0 +1,8 @@ +Raise or warn on registering a colormap twice +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using `matplotlib.cm.register_cmap` to register a user provided +or third-party colormap it will now raise a `ValueError` if trying to +over-write one of the built in colormaps and warn if trying to over +write a user registered colormap. This may raise for user-registered +colormaps in the future. diff --git a/doc/users/next_whats_new/2019-08_tac.rst b/doc/users/next_whats_new/2019-08_tac.rst new file mode 100644 index 000000000000..3e7e3648b421 --- /dev/null +++ b/doc/users/next_whats_new/2019-08_tac.rst @@ -0,0 +1,6 @@ + +Add ``cm.unregister_cmap`` function +----------------------------------- + +`.cm.unregister_cmap` allows users to remove a colormap that they +have previously registered. diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index b8a67550bff3..d4a8b1df5cb2 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -24,6 +24,7 @@ from matplotlib import _api, colors, cbook from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed +from matplotlib.cbook import _warn_external LUTSIZE = mpl.rcParams['image.lut'] @@ -95,30 +96,38 @@ def _warn_deprecated(self): locals().update(_cmap_registry) # This is no longer considered public API cmap_d = _DeprecatedCmapDictWrapper(_cmap_registry) - +__builtin_cmaps = tuple(_cmap_registry) # Continue with definitions ... -def register_cmap(name=None, cmap=None, data=None, lut=None): +def register_cmap(name=None, cmap=None, *, override_builtin=False): """ Add a colormap to the set recognized by :func:`get_cmap`. - It can be used in two ways:: + Register a new colormap to be accessed by name :: + + LinearSegmentedColormap('swirly', data, lut) + register_cmap(cmap=swirly_cmap) - register_cmap(name='swirly', cmap=swirly_cmap) + Parameters + ---------- + name : str, optional + The name that can be used in :func:`get_cmap` or :rc:`image.cmap` - register_cmap(name='choppy', data=choppydata, lut=128) + If absent, the name will be the :attr:`~matplotlib.colors.Colormap.name` + attribute of the *cmap*. - In the first case, *cmap* must be a :class:`matplotlib.colors.Colormap` - instance. The *name* is optional; if absent, the name will - be the :attr:`~matplotlib.colors.Colormap.name` attribute of the *cmap*. + cmap : matplotlib.colors.Colormap + Despite being the second argument and having a default value, this + is a required argument. - The second case is deprecated. Here, the three arguments are passed to - the :class:`~matplotlib.colors.LinearSegmentedColormap` initializer, - 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))``. + override_builtin : bool + + Allow built-in colormaps to be overridden by a user-supplied + colormap. + + Please do not use this unless you are sure you need it. Notes ----- @@ -126,6 +135,7 @@ def register_cmap(name=None, cmap=None, data=None, lut=None): 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: @@ -134,23 +144,21 @@ def register_cmap(name=None, cmap=None, data=None, lut=None): except AttributeError as err: raise ValueError("Arguments must include a name or a " "Colormap") from err - if isinstance(cmap, colors.Colormap): - cmap._global = True - _cmap_registry[name] = cmap - return - if lut is not None or data is not None: - cbook.warn_deprecated( - "3.3", - message="Passing raw data via parameters data and lut to " - "register_cmap() is deprecated since %(since)s and will " - "become an error %(removal)s. Instead use: register_cmap(" - "cmap=LinearSegmentedColormap(name, data, lut))") - # For the remainder, let exceptions propagate. - if lut is None: - lut = mpl.rcParams['image.lut'] - cmap = colors.LinearSegmentedColormap(name, data, lut) + if name in _cmap_registry: + if not override_builtin and name in __builtin_cmaps: + msg = f"Trying to re-register the builtin cmap {name!r}." + raise ValueError(msg) + else: + msg = f"Trying to register the cmap {name!r} which already exists." + _warn_external(msg) + + if not isinstance(cmap, colors.Colormap): + raise ValueError("You must pass a Colormap instance. " + f"You passed {cmap} a {type(cmap)} object.") + cmap._global = True _cmap_registry[name] = cmap + return def get_cmap(name=None, lut=None): @@ -187,6 +195,47 @@ def get_cmap(name=None, lut=None): return _cmap_registry[name]._resample(lut) +def unregister_cmap(name): + """ + Remove a colormap recognized by :func:`get_cmap`. + + You may not remove built-in colormaps. + + If the named colormap is not registered, returns with no error, raises + if you try to de-register a default colormap. + + .. warning :: + + Colormap names are currently a shared namespace that may be used + by multiple packages. Use `unregister_cmap` only if you know you + have registered that name before. In particular, do not + unregister just in case to clean the name before registering a + new colormap. + + Parameters + ---------- + name : str + The name of the colormap to be un-registered + + Returns + ------- + ColorMap or None + If the colormap was registered, return it if not return `None` + + Raises + ------ + ValueError + If you try to de-register a default built-in colormap. + + """ + if name not in _cmap_registry: + return + if name in __builtin_cmaps: + raise ValueError(f"cannot unregister {name!r} which is a builtin " + "colormap.") + return _cmap_registry.pop(name) + + class ScalarMappable: """ A mixin class to map scalar data to RGBA. diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 3e8226b0ce25..9500e5234ff1 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -652,7 +652,7 @@ def get_bad(self): """Get the color for masked values.""" if not self._isinit: self._init() - return self._lut[self._i_bad] + return np.array(self._lut[self._i_bad]) def set_bad(self, color='k', alpha=None): """Set the color for masked values.""" @@ -665,7 +665,7 @@ def get_under(self): """Get the color for low out-of-range values.""" if not self._isinit: self._init() - return self._lut[self._i_under] + return np.array(self._lut[self._i_under]) def set_under(self, color='k', alpha=None): """Set the color for low out-of-range values.""" @@ -678,7 +678,7 @@ def get_over(self): """Get the color for high out-of-range values.""" if not self._isinit: self._init() - return self._lut[self._i_over] + return np.array(self._lut[self._i_over]) def set_over(self, color='k', alpha=None): """Set the color for high out-of-range values.""" diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index d180fb28afa5..8a1a96b47389 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -64,14 +64,44 @@ def test_resample(): def test_register_cmap(): - new_cm = copy.copy(plt.cm.viridis) - cm.register_cmap('viridis2', new_cm) - assert plt.get_cmap('viridis2') == new_cm + new_cm = copy.copy(cm.get_cmap("viridis")) + target = "viridis2" + cm.register_cmap(target, new_cm) + assert plt.get_cmap(target) == new_cm with pytest.raises(ValueError, - match='Arguments must include a name or a Colormap'): + match="Arguments must include a name or a Colormap"): cm.register_cmap() + with pytest.warns(UserWarning): + cm.register_cmap(target, new_cm) + + cm.unregister_cmap(target) + with pytest.raises(ValueError, + match=f'{target!r} is not a valid value for name;'): + cm.get_cmap(target) + # test that second time is error free + cm.unregister_cmap(target) + + with pytest.raises(ValueError, match="You must pass a Colormap instance."): + cm.register_cmap('nome', cmap='not a cmap') + + +def test_double_register_builtin_cmap(): + name = "viridis" + match = f"Trying to re-register the builtin cmap {name!r}." + with pytest.raises(ValueError, match=match): + cm.register_cmap(name, cm.get_cmap(name)) + with pytest.warns(UserWarning): + cm.register_cmap(name, cm.get_cmap(name), override_builtin=True) + + +def test_unregister_builtin_cmap(): + name = "viridis" + match = f'cannot unregister {name!r} which is a builtin colormap.' + with pytest.raises(ValueError, match=match): + cm.unregister_cmap(name) + def test_colormap_global_set_warn(): new_cm = plt.get_cmap('viridis') @@ -94,7 +124,8 @@ def test_colormap_global_set_warn(): new_cm.set_under('k') # Re-register the original - plt.register_cmap(cmap=orig_cmap) + with pytest.warns(UserWarning): + plt.register_cmap(cmap=orig_cmap, override_builtin=True) def test_colormap_dict_deprecate(): @@ -1187,6 +1218,16 @@ def test_get_under_over_bad(): assert_array_equal(cmap.get_bad(), cmap(np.nan)) +@pytest.mark.parametrize('kind', ('over', 'under', 'bad')) +def test_non_mutable_get_values(kind): + cmap = copy.copy(plt.get_cmap('viridis')) + init_value = getattr(cmap, f'get_{kind}')() + getattr(cmap, f'set_{kind}')('k') + black_value = getattr(cmap, f'get_{kind}')() + assert np.all(black_value == [0, 0, 0, 1]) + assert not np.all(init_value == black_value) + + def test_colormap_alpha_array(): cmap = plt.get_cmap('viridis') vals = [-1, 0.5, 2] # under, valid, over pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy