From 9ba6d17c28e3254005f68ba93abede59356a3851 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sat, 3 May 2025 18:33:59 +0100 Subject: [PATCH 1/3] Include close matches when key not found --- lib/matplotlib/__init__.py | 9 ++++----- lib/matplotlib/_api/__init__.py | 23 +++++++++++++++++++---- lib/matplotlib/_api/__init__.pyi | 6 ++++-- lib/matplotlib/cm.py | 8 ++++---- lib/matplotlib/tests/test_colors.py | 10 ++++++++++ lib/matplotlib/tests/test_style.py | 4 +++- 6 files changed, 44 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 9abc6c5a84dd..f4622748c0b0 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, _suggest_close_matches=True, _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..19893310f502 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,21 @@ def check_shape(shape, /, **kwargs): ) -def check_getitem(mapping, /, **kwargs): +def check_getitem( + mapping, /, _suggest_close_matches=False, _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 + ---------- + _suggest_close_matches : + If True, suggest only close matches instead of all valid values. + _error_cls : + Class of error to raise. + Examples -------- >>> _api.check_getitem({"foo": "bar"}, arg=arg) @@ -190,9 +200,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 _suggest_close_matches: + 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..3a8aaa81b674 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, Any], /, _suggest_close_matches: bool, _error_cls: Type[Exception], **kwargs: Any +) -> Any: ... 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..f4deb3e82d0f 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, _suggest_close_matches=True, _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 From a0b131d7b6735fdbc71c78a8cbb972a95b3d8202 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sat, 3 May 2025 18:37:24 +0100 Subject: [PATCH 2/3] Improve return type of check_getitem --- lib/matplotlib/_api/__init__.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 3a8aaa81b674..e213dfd23e44 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -43,8 +43,8 @@ def check_in_list( ) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... def check_getitem( - mapping: Mapping[Any, Any], /, _suggest_close_matches: bool, _error_cls: Type[Exception], **kwargs: Any -) -> Any: ... + mapping: Mapping[Any, _T], /, _suggest_close_matches: bool, _error_cls: Type[Exception], **kwargs: Any +) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( From 91520390942f9966191394d459c6f6edc24041c4 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Jun 2025 10:14:05 +0100 Subject: [PATCH 3/3] Automatically determine whether to suggest options --- lib/matplotlib/__init__.py | 2 +- lib/matplotlib/_api/__init__.py | 6 ++---- lib/matplotlib/_api/__init__.pyi | 2 +- lib/matplotlib/cm.py | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index f4622748c0b0..57b0bd6582c8 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -741,7 +741,7 @@ def __setitem__(self, key, val): and "backend" in self): return valid_key = _api.check_getitem( - self.validate, rcParam=key, _suggest_close_matches=True, _error_cls=KeyError + self.validate, rcParam=key, _error_cls=KeyError ) try: cval = valid_key(val) diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 19893310f502..13f291e1eb31 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -176,7 +176,7 @@ def check_shape(shape, /, **kwargs): def check_getitem( - mapping, /, _suggest_close_matches=False, _error_cls=ValueError, **kwargs + mapping, /, _error_cls=ValueError, **kwargs ): """ *kwargs* must consist of a single *key, value* pair. If *key* is in @@ -185,8 +185,6 @@ def check_getitem( Parameters ---------- - _suggest_close_matches : - If True, suggest only close matches instead of all valid values. _error_cls : Class of error to raise. @@ -200,7 +198,7 @@ def check_getitem( try: return mapping[v] except KeyError: - if _suggest_close_matches: + 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: diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index e213dfd23e44..f6a3d49e60a3 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -43,7 +43,7 @@ def check_in_list( ) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... def check_getitem( - mapping: Mapping[Any, _T], /, _suggest_close_matches: bool, _error_cls: Type[Exception], **kwargs: Any + mapping: Mapping[Any, _T], /, _error_cls: Type[Exception], **kwargs: Any ) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index f4deb3e82d0f..f38baae3b80e 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -93,7 +93,7 @@ def __init__(self, cmaps): def __getitem__(self, item): cmap = _api.check_getitem( - self._cmaps, colormap=item, _suggest_close_matches=True, _error_cls=KeyError + self._cmaps, colormap=item, _error_cls=KeyError ) return cmap.copy()