Skip to content

Commit 192b7c2

Browse files
dstansbyQuLogic
andauthored
Include close matches in error message when key not found (matplotlib#30001)
* Include close matches when key not found * Improve return type of check_getitem * Automatically determine whether to suggest options * Remove deprecated import * Style fixes Co-authored-by: Elliott Sales de Andrade <[email protected]> --------- Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent efacc7d commit 192b7c2

File tree

6 files changed

+37
-15
lines changed

6 files changed

+37
-15
lines changed

lib/matplotlib/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -743,12 +743,11 @@ def __setitem__(self, key, val):
743743
and val is rcsetup._auto_backend_sentinel
744744
and "backend" in self):
745745
return
746+
valid_key = _api.check_getitem(
747+
self.validate, rcParam=key, _error_cls=KeyError
748+
)
746749
try:
747-
cval = self.validate[key](val)
748-
except KeyError as err:
749-
raise KeyError(
750-
f"{key} is not a valid rc parameter (see rcParams.keys() for "
751-
f"a list of valid parameters)") from err
750+
cval = valid_key(val)
752751
except ValueError as ve:
753752
raise ValueError(f"Key {key}: {ve}") from None
754753
self._set(key, cval)

lib/matplotlib/_api/__init__.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
1111
"""
1212

13+
import difflib
1314
import functools
1415
import itertools
1516
import pathlib
@@ -174,12 +175,17 @@ def check_shape(shape, /, **kwargs):
174175
)
175176

176177

177-
def check_getitem(mapping, /, **kwargs):
178+
def check_getitem(mapping, /, _error_cls=ValueError, **kwargs):
178179
"""
179180
*kwargs* must consist of a single *key, value* pair. If *key* is in
180181
*mapping*, return ``mapping[value]``; else, raise an appropriate
181182
ValueError.
182183
184+
Parameters
185+
----------
186+
_error_cls :
187+
Class of error to raise.
188+
183189
Examples
184190
--------
185191
>>> _api.check_getitem({"foo": "bar"}, arg=arg)
@@ -190,9 +196,14 @@ def check_getitem(mapping, /, **kwargs):
190196
try:
191197
return mapping[v]
192198
except KeyError:
193-
raise ValueError(
194-
f"{v!r} is not a valid value for {k}; supported values are "
195-
f"{', '.join(map(repr, mapping))}") from None
199+
if len(mapping) > 5:
200+
if len(best := difflib.get_close_matches(v, mapping.keys(), cutoff=0.5)):
201+
suggestion = f"Did you mean one of {best}?"
202+
else:
203+
suggestion = ""
204+
else:
205+
suggestion = f"Supported values are {', '.join(map(repr, mapping))}"
206+
raise _error_cls(f"{v!r} is not a valid value for {k}. {suggestion}") from None
196207

197208

198209
def caching_module_getattr(cls):

lib/matplotlib/_api/__init__.pyi

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ def check_in_list(
4242
values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any
4343
) -> None: ...
4444
def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ...
45-
def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ...
45+
def check_getitem(
46+
mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any
47+
) -> _T: ...
4648
def caching_module_getattr(cls: type) -> Callable[[str], Any]: ...
4749
@overload
4850
def define_aliases(

lib/matplotlib/cm.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,8 @@ def __init__(self, cmaps):
9292
self._builtin_cmaps = tuple(cmaps)
9393

9494
def __getitem__(self, item):
95-
try:
96-
return self._cmaps[item].copy()
97-
except KeyError:
98-
raise KeyError(f"{item!r} is not a known colormap name") from None
95+
cmap = _api.check_getitem(self._cmaps, colormap=item, _error_cls=KeyError)
96+
return cmap.copy()
9997

10098
def __iter__(self):
10199
return iter(self._cmaps)

lib/matplotlib/tests/test_colors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,3 +1876,13 @@ def scaled(self):
18761876
axes[0,1].pcolor(r, colorizer=colorizer)
18771877
axes[1,0].contour(r, colorizer=colorizer)
18781878
axes[1,1].contourf(r, colorizer=colorizer)
1879+
1880+
1881+
def test_close_error_name():
1882+
with pytest.raises(
1883+
KeyError,
1884+
match=(
1885+
"'grays' is not a valid value for colormap. "
1886+
"Did you mean one of ['gray', 'Grays', 'gray_r']?"
1887+
)):
1888+
matplotlib.colormaps["grays"]

lib/matplotlib/tests/test_style.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ def test_context_with_badparam():
150150
with style.context({PARAM: other_value}):
151151
assert mpl.rcParams[PARAM] == other_value
152152
x = style.context({PARAM: original_value, 'badparam': None})
153-
with pytest.raises(KeyError):
153+
with pytest.raises(
154+
KeyError, match="\'badparam\' is not a valid value for rcParam. "
155+
):
154156
with x:
155157
pass
156158
assert mpl.rcParams[PARAM] == other_value

0 commit comments

Comments
 (0)