From fcf1c0c97dd91fb989e83171140e0eee6282bc41 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Oct 2022 18:00:01 -0400 Subject: [PATCH 1/5] FIX: add missing method to ColormapRegistry After putting pending deprecations on `cm.get_cmap` we discovered that downstream libraries (pandas) were using the deprecated method to normalize between `None` (to get the default colormap), strings, and Colormap instances. This adds a method to `ColormapRegistry` to do this normalization. This can not replace our internal helper due to variations in what exceptions are raised. Closes #23981 --- .../api_changes_3.6.0/deprecations.rst | 10 ++++- lib/matplotlib/cm.py | 43 ++++++++++++++++++- lib/matplotlib/tests/test_colors.py | 15 +++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst index d59077d2b2d2..028262af43d0 100644 --- a/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst @@ -52,7 +52,13 @@ In Matplotlib 3.6 we have marked those top level functions as pending deprecation with the intention of deprecation in Matplotlib 3.7. The following functions have been marked for pending deprecation: -- ``matplotlib.cm.get_cmap``; use ``matplotlib.colormaps[name]`` instead +- ``matplotlib.cm.get_cmap``; use ``matplotlib.colormaps[name]`` instead if you + have a `str`. + + **Added 3.6.1** Use `matplotlib.cm.ColormapRegistry.get_cmap` if you + have a string, `None` or a `matplotlib.colors.Colormap` object that you want + to convert to a `matplotlib.colors.Colormap` instance. Raises `KeyError` + rather than `ValueError` for missing strings. - ``matplotlib.cm.register_cmap``; use `matplotlib.colormaps.register <.ColormapRegistry.register>` instead - ``matplotlib.cm.unregister_cmap``; use `matplotlib.colormaps.unregister @@ -305,7 +311,7 @@ Backend-specific deprecations private functions if you rely on it. - ``backend_svg.generate_transform`` and ``backend_svg.generate_css`` - ``backend_tk.NavigationToolbar2Tk.lastrect`` and - ``backend_tk.RubberbandTk.lastrect`` + ``backend_tk.RubberbandTk.lastrect`` - ``backend_tk.NavigationToolbar2Tk.window``; use ``toolbar.master`` instead. - ``backend_tools.ToolBase.destroy``; To run code upon tool removal, connect to the ``tool_removed_event`` event. diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index f6e5ee8b7156..c138fb14e907 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -193,6 +193,39 @@ def unregister(self, name): "colormap.") self._cmaps.pop(name, None) + def get_cmap(self, cmap): + """ + Ensure that at given object is a converted to a color map. + + If *cmap* in `None`, returns the Colormap named by :rc:`image.cmap`. + + Parameters + ---------- + cmap : str, Colormap, None + + - if a `~matplotlib.colors.Colormap`, return it + - if a string, look it up in mpl.colormaps + - if None, look up the default color map in mpl.colormaps + + Returns + ------- + Colormap + + Raises + ------ + KeyError + """ + # get the default color map + if cmap is None: + return self[mpl.rcParams["image.cmap"]] + + # if the user passed in a Colormap, simply return it + if isinstance(cmap, colors.Colormap): + return cmap + + # otherwise, it must be a string so look it up + return self[cmap] + # public access to the colormaps should be via `matplotlib.colormaps`. For now, # we still create the registry here, but that should stay an implementation @@ -281,7 +314,12 @@ def _get_cmap(name=None, lut=None): # pyplot. get_cmap = _api.deprecated( '3.6', - name='get_cmap', pending=True, alternative="``matplotlib.colormaps[name]``" + name='get_cmap', + pending=True, + alternative=( + "``matplotlib.colormaps[name]`` " + + "or ``matplotlib.colormaps.get_cmap(obj)``" + ) )(_get_cmap) @@ -687,6 +725,8 @@ def _ensure_cmap(cmap): """ Ensure that we have a `.Colormap` object. + For internal use to preserve type stability of errors. + Parameters ---------- cmap : None, str, Colormap @@ -698,6 +738,7 @@ def _ensure_cmap(cmap): Returns ------- Colormap + """ if isinstance(cmap, colors.Colormap): return cmap diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index f0c23038e11a..711fecf43e11 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -109,6 +109,21 @@ def test_register_cmap(): cm.register_cmap('nome', cmap='not a cmap') +def test_ensure_cmap(): + cr = mpl.colormaps + new_cm = mcolors.ListedColormap(cr["viridis"].colors, name='v2') + + # check None, str, and Colormap pass + assert cr.get_cmap('plasma') == cr["plasma"] + assert cr.get_cmap(cr["magma"]) == cr["magma"] + + # check default default + assert cr.get_cmap(None) == cr[mpl.rcParams['image.cmap']] + bad_cmap = 'AardvarksAreAwkward' + with pytest.raises(KeyError, match=bad_cmap): + cr.get_cmap(bad_cmap) + + def test_double_register_builtin_cmap(): name = "viridis" match = f"Re-registering the builtin cmap {name!r}." From 3f99c20e649fd0d533332ee4e3b4102e07d0b7f2 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 6 Oct 2022 18:47:56 -0400 Subject: [PATCH 2/5] DOC: remove note about ColormapRegistry being experimental We are committed now! --- lib/matplotlib/cm.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index c138fb14e907..20842f0e19c7 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -61,12 +61,6 @@ class ColormapRegistry(Mapping): r""" Container for colormaps that are known to Matplotlib by name. - .. admonition:: Experimental - - While we expect the API to be final, we formally mark it as - experimental for 3.5 because we want to keep the option to still adapt - the API for 3.6 should the need arise. - The universal registry instance is `matplotlib.colormaps`. There should be no need for users to instantiate `.ColormapRegistry` themselves. From b8bdcf84e6f52c70078f6eeb49f0c420464d3425 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Oct 2022 15:15:16 -0400 Subject: [PATCH 3/5] DOC: fix formatting and wording Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/cm.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 20842f0e19c7..254efdd95fca 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -189,17 +189,15 @@ def unregister(self, name): def get_cmap(self, cmap): """ - Ensure that at given object is a converted to a color map. - - If *cmap* in `None`, returns the Colormap named by :rc:`image.cmap`. + Return a color map specified through *cmap*. Parameters ---------- - cmap : str, Colormap, None + cmap : str or `~matplotlib.colors.Colormap` or None - - if a `~matplotlib.colors.Colormap`, return it - - if a string, look it up in mpl.colormaps - - if None, look up the default color map in mpl.colormaps + - if a `.Colormap`, return it + - if a string, look it up in ``mpl.colormaps`` + - if None, return the Colormap defined in :rc:`image.cmap` Returns ------- From d0a240a1170c60be449a9ad7de9d3d30ab417f92 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Oct 2022 15:15:29 -0400 Subject: [PATCH 4/5] MNT: fix test name Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/tests/test_colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 711fecf43e11..3b4a775c5379 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -109,7 +109,7 @@ def test_register_cmap(): cm.register_cmap('nome', cmap='not a cmap') -def test_ensure_cmap(): +def test_colormaps_get_cmap(): cr = mpl.colormaps new_cm = mcolors.ListedColormap(cr["viridis"].colors, name='v2') From 4f8ece457ba0d979813d6e992c3d6562cd7db2d0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Oct 2022 17:10:04 -0400 Subject: [PATCH 5/5] MNT: raise ValueError and TypeError rather than KeyError --- .../api_changes_3.6.0/deprecations.rst | 3 +-- lib/matplotlib/cm.py | 15 ++++++++------- lib/matplotlib/tests/test_colors.py | 13 +++++++++---- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst index 028262af43d0..3a9e91e12289 100644 --- a/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst @@ -57,8 +57,7 @@ functions have been marked for pending deprecation: **Added 3.6.1** Use `matplotlib.cm.ColormapRegistry.get_cmap` if you have a string, `None` or a `matplotlib.colors.Colormap` object that you want - to convert to a `matplotlib.colors.Colormap` instance. Raises `KeyError` - rather than `ValueError` for missing strings. + to convert to a `matplotlib.colors.Colormap` instance. - ``matplotlib.cm.register_cmap``; use `matplotlib.colormaps.register <.ColormapRegistry.register>` instead - ``matplotlib.cm.unregister_cmap``; use `matplotlib.colormaps.unregister diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 254efdd95fca..ec0d472992ef 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -202,10 +202,6 @@ def get_cmap(self, cmap): Returns ------- Colormap - - Raises - ------ - KeyError """ # get the default color map if cmap is None: @@ -214,9 +210,14 @@ def get_cmap(self, cmap): # if the user passed in a Colormap, simply return it if isinstance(cmap, colors.Colormap): return cmap - - # otherwise, it must be a string so look it up - return self[cmap] + if isinstance(cmap, str): + _api.check_in_list(sorted(_colormaps), cmap=cmap) + # otherwise, it must be a string so look it up + return self[cmap] + raise TypeError( + 'get_cmap expects None or an instance of a str or Colormap . ' + + f'you passed {cmap!r} of type {type(cmap)}' + ) # public access to the colormaps should be via `matplotlib.colormaps`. For now, diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 3b4a775c5379..86536ab17234 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -111,18 +111,23 @@ def test_register_cmap(): def test_colormaps_get_cmap(): cr = mpl.colormaps - new_cm = mcolors.ListedColormap(cr["viridis"].colors, name='v2') - # check None, str, and Colormap pass + # check str, and Colormap pass assert cr.get_cmap('plasma') == cr["plasma"] assert cr.get_cmap(cr["magma"]) == cr["magma"] - # check default default + # check default assert cr.get_cmap(None) == cr[mpl.rcParams['image.cmap']] + + # check ValueError on bad name bad_cmap = 'AardvarksAreAwkward' - with pytest.raises(KeyError, match=bad_cmap): + with pytest.raises(ValueError, match=bad_cmap): cr.get_cmap(bad_cmap) + # check TypeError on bad type + with pytest.raises(TypeError, match='object'): + cr.get_cmap(object()) + def test_double_register_builtin_cmap(): name = "viridis"