diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 9abc6c5a84dd..57b0bd6582c8 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -740,12 +740,11 @@ def __setitem__(self, key, val): and val is rcsetup._auto_backend_sentinel and "backend" in self): return + valid_key = _api.check_getitem( + self.validate, rcParam=key, _error_cls=KeyError + ) try: - cval = self.validate[key](val) - except KeyError as err: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") from err + cval = valid_key(val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None self._set(key, cval) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 47c32f701729..13f291e1eb31 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -10,6 +10,7 @@ """ +import difflib import functools import itertools import pathlib @@ -174,12 +175,19 @@ def check_shape(shape, /, **kwargs): ) -def check_getitem(mapping, /, **kwargs): +def check_getitem( + mapping, /, _error_cls=ValueError, **kwargs + ): """ *kwargs* must consist of a single *key, value* pair. If *key* is in *mapping*, return ``mapping[value]``; else, raise an appropriate ValueError. + Parameters + ---------- + _error_cls : + Class of error to raise. + Examples -------- >>> _api.check_getitem({"foo": "bar"}, arg=arg) @@ -190,9 +198,14 @@ def check_getitem(mapping, /, **kwargs): try: return mapping[v] except KeyError: - raise ValueError( - f"{v!r} is not a valid value for {k}; supported values are " - f"{', '.join(map(repr, mapping))}") from None + if len(mapping) > 5: + if len(best := difflib.get_close_matches(v, mapping.keys(), cutoff=0.5)): + suggestion = f"Did you mean one of {best}?" + else: + suggestion = "" + else: + suggestion = f"Supported values are {', '.join(map(repr, mapping))}" + raise _error_cls(f"{v!r} is not a valid value for {k}. {suggestion}") from None def caching_module_getattr(cls): diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 9bf67110bb54..f6a3d49e60a3 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable, Generator, Iterable, Mapping, Sequence -from typing import Any, TypeVar, overload +from typing import Any, Type, TypeVar, overload from typing_extensions import Self # < Py 3.11 from numpy.typing import NDArray @@ -42,7 +42,9 @@ def check_in_list( values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any ) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... -def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ... +def check_getitem( + mapping: Mapping[Any, _T], /, _error_cls: Type[Exception], **kwargs: Any +) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index ef5bf0719d3b..f38baae3b80e 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -92,10 +92,10 @@ def __init__(self, cmaps): self._builtin_cmaps = tuple(cmaps) def __getitem__(self, item): - try: - return self._cmaps[item].copy() - except KeyError: - raise KeyError(f"{item!r} is not a known colormap name") from None + cmap = _api.check_getitem( + self._cmaps, colormap=item, _error_cls=KeyError + ) + return cmap.copy() def __iter__(self): return iter(self._cmaps) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index df3f65bdb2dc..e8b554ba27d9 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1829,3 +1829,13 @@ def test_LinearSegmentedColormap_from_list_value_color_tuple(): cmap([value for value, _ in value_color_tuples]), to_rgba_array([color for _, color in value_color_tuples]), ) + + +def test_close_error_name(): + with pytest.raises( + KeyError, + match=( + "'grays' is not a valid value for colormap. " + "Did you mean one of ['gray', 'Grays', 'gray_r']?" + )): + matplotlib.colormaps["grays"] diff --git a/lib/matplotlib/tests/test_style.py b/lib/matplotlib/tests/test_style.py index be038965e33d..de116af6c89b 100644 --- a/lib/matplotlib/tests/test_style.py +++ b/lib/matplotlib/tests/test_style.py @@ -140,7 +140,9 @@ def test_context_with_badparam(): with style.context({PARAM: other_value}): assert mpl.rcParams[PARAM] == other_value x = style.context({PARAM: original_value, 'badparam': None}) - with pytest.raises(KeyError): + with pytest.raises( + KeyError, match="\'badparam\' is not a valid value for rcParam. " + ): with x: pass assert mpl.rcParams[PARAM] == other_value