Repository: matplotlib/matplotlib
Pull request: FIX: add missing method to ColormapRegistry
## PR Summary

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

I opted to edit the existing deprecation message as this seemed like the lest-bad option.

## PR Checklist

<!-- Please mark any checkboxes that do not apply to this PR as [N/A]. -->
**Tests and Styling**
- [x] Has pytest style unit tests (and `pytest` passes).
- [x] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (install `flake8-docstrings` and run `flake8 --docstring-convention=all`).

**Documentation**
- [ ] New features are documented, with examples if plot related.
- [ ] New features have an entry in `doc/users/next_whats_new/` (follow instructions in README.rst there).
- [x] API changes documented in `doc/api/next_api_changes/` (follow instructions in README.rst there).
- [x] Documentation is sphinx and numpydoc compliant (the docs should [build](https://matplotlib.org/devel/documenting_mpl.html#building-the-docs) without error).


Problem statement:
[ENH]: Default `matplotlib.colormaps[None]` to call `matplotlib.colormaps[matplotlib.rcParams['image.cmap']]`?
### Problem

While addressing the `matplotlib.cm.get_cmap` deprecation in 3.6:

```
PendingDeprecationWarning: The get_cmap function will be deprecated in a future version. Use ``matplotlib.colormaps[name]`` instead.
```
I noticed that `None` isn't directly migrate-able 

```
In [1]: import matplotlib

In [2]: matplotlib.cm.get_cmap(None)
Out[2]: <matplotlib.colors.ListedColormap at 0x11e609e20>

In [3]: matplotlib.colormaps[None]
KeyError: 'None is not a known colormap name'
```

### Proposed solution

It appears from the source that `get_cmap(None)` defaults to  `matplotlib.rcParams['image.cmap']` so it would be nice if `colormaps[None]` could default to that as well.

Otherwise, it would be nice if this was better documented.

Hints from issue discussion:
If folks agree it's a regression error (& I think it kinda is) the fix is I think add something like

```python
 if item is None: 
     item = mpl.rcParams['image.cmap'] 
```
which is a direct port of the `get_cmap` code 
https://github.com/matplotlib/matplotlib/blob/a152851669b9df06302d3a133c7413b863f00283/lib/matplotlib/cm.py#L270-L271

before the try in:

https://github.com/matplotlib/matplotlib/blob/a152851669b9df06302d3a133c7413b863f00283/lib/matplotlib/cm.py#L91-L95

@mroeschke What is your use case? Are you passing in `None` as a literal or is it the value of some variable you have? Is this in library code or do you expect this to be needed in an interactive console.

We have discussed this on the last dev call and are not yet clear how to handle the default colormap case in the new API. Logically, `colormaps` itself was designed as a mapping of all explicit colormaps. The default colormap notion does not fit in there very well. It depends on the anticipated usage how we would handle that API-wise.

The use case would be for library code. In pandas, there are some APIs with matplotlib functionality that accept `cmap` that default to `None` and were passed into `matplotlib.cm.get_cmap`: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.io.formats.style.Styler.bar.html

We're happy to start using `matplotlib.colormaps` instead just noting in https://github.com/matplotlib/matplotlib/issues/23981#issue-1381487093 that I had to dig into what `get_cmap(None)` did before to maintain the prior behavior.  
@mroeschke I was going to suggest that you do something like

```
cmap = cmap in cmap is not None else mpl.rcparams['image.cmap']
```

which should work on all versions of Matplotlib and leaves you an escape hatch if you want to make the default colormap controllable indepently.  However, that probably is not enough for you as if you take `str` or `ColorMap` and actually want the color map (rather than passing through to some mpl function) you'll also have to have the logic to decide if you need to look up the color map or if the user directly gave you one.


When we decided to move to the registry model we mostly had end-users in mind, however this issues highlights that we did not take into account the library case enough.  We have https://github.com/matplotlib/matplotlib/blob/0517187b9c91061d2ec87e70442615cf4f47b6f3/lib/matplotlib/cm.py#L686-L708 for internal use (which is a bit more complicated than it needs to be to preserve exact error types).  It is not clear to me if we should make that public or document the pattern of:

```python
if isinstance(inp, mcolors.Colormap):
    cmap = inp
else:
    cmap = mpl.colormaps[inp if inp is not None else mpl.rcParams["image.cmap"]]

```
There is just enough complexity in there it probably be a function we provide. I think this has to be a function/method rather than shoehorned into `__getitem__` or `get` on the registry because that would be getting too far away from the `Mapping` API

If we have a registry method for this I propose

```python
def ensure_colormap(self, cmap: str|Colormap, * default=None):
    if isinstance(cmap, mcolors.Colormap):
       return cmap

    default = default if default is not None else mpl.rcParams["image.cmap"]
    cmap = cmap if cmap is not None else default
    return self[cmap]
```
which could also live as a free method and use the global registry.
Yeah in pandas we used a similar workaround for translating `cm.cmap(None)` to remove the deprecation warning.

I don't have any strong opinions personally on `colormaps[None]` being valid. I mainly wanted to highlighting that the deprecation message wasn't entirely clear for the `name=None` case.
Discussed on the call, we are going to put the method on the registery class for 3.6.1 on the reasoning that this is not a new feature, but completing something that should have been in 3.6.1 as part of the deplication.

Commit message: DOC: remove note about ColormapRegistry being experimental

We are committed now!

Patch to review:
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..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.
 
@@ -193,6 +187,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 +308,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 +719,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 +732,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}."


Code review comments:
diff_hunk: @@ -193,6 +187,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
path: lib/matplotlib/cm.py
text: @user1:
We may want to transform this into a ValueError. `get_cmap` is less a lookup and more a transformation function.

@author:
I have a slight preference for `KeyError` to mirror `__getitem__`.  Maybe it should raise both and add another check that `cmap` is actually a string and then raise a `ValueError`?  There are ways to call this now that will get "not hashable" errors etc.

@user2:
If you're going to check strings, then I'd think it would be `if isinstance(cmap, str): return valid or raise ValueError; else: raise TypeError`, and maybe no `KeyError` at all?

diff_hunk: @@ -193,6 +187,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`.
path: lib/matplotlib/cm.py
text: @user1:
```suggestion
        Return a color map specified through *cmap*.
```