Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b07211f

Browse files
authored
Add suggestions to more lookup errors (matplotlib#31514)
* Tweak wording of suggestion message from getitem_checked No need for 'one of' if there's only one option, add a colon to match the syntax of Python's own suggestion message, and remove the trailing space if there's no suggestion. * Add suggestions to ColorSequenceRegistry lookup errors * Remove _print_supported_values parameter from _api.check_in_list The only usage was removed in 8cde5a8. * Harmonize error messages from _api.check_in_list and _api.getitem_checked This also means that the former now gains suggestions. * Add suggestions to ProjectionRegistry lookup errors * Add suggestions to MovieWriterRegistry lookup errors * Add suggestions to Colorizer cmap lookup errors
1 parent fb0cb05 commit b07211f

11 files changed

Lines changed: 72 additions & 41 deletions

File tree

lib/matplotlib/_api/__init__.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,37 @@ def type_name(tp):
155155
type_name(type(v))))
156156

157157

158-
def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
158+
def list_suggestion_error_msg(name, potential, values):
159+
"""
160+
Generate an error message that a potential setting is not an acceptable value.
161+
162+
If the acceptable values are all strings, and sufficiently large, then add just a
163+
few suggestions to the end of the message. Otherwise list the supported values.
164+
165+
Parameters
166+
----------
167+
name : str
168+
The name of the setting, keyword argument, etc. to generate the message for.
169+
potential
170+
The potential value from the user that is not a valid choice.
171+
values : iterable
172+
Sequence of values to check on.
173+
"""
174+
if len(values) > 5 and all(isinstance(v, str) for v in [potential, *values]):
175+
best = difflib.get_close_matches(potential, values, cutoff=0.5)
176+
match len(best):
177+
case 0:
178+
suggestion = ""
179+
case 1:
180+
suggestion = f" Did you mean: {best[0]!r}?"
181+
case _:
182+
suggestion = f" Did you mean one of: {', '.join(map(repr, best))}?"
183+
else:
184+
suggestion = f" Supported values are {', '.join(map(repr, values))}"
185+
return f"{potential!r} is not a valid value for {name}.{suggestion}"
186+
187+
188+
def check_in_list(values, /, **kwargs):
159189
"""
160190
For each *key, value* pair in *kwargs*, check that *value* is in *values*;
161191
if not, raise an appropriate ValueError.
@@ -167,8 +197,6 @@ def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
167197
168198
Note: All values must support == comparisons.
169199
This means in particular the entries must not be numpy arrays.
170-
_print_supported_values : bool, default: True
171-
Whether to print *values* when raising ValueError.
172200
**kwargs : dict
173201
*key, value* pairs as keyword arguments to find in *values*.
174202
@@ -196,10 +224,7 @@ def check_in_list(values, /, *, _print_supported_values=True, **kwargs):
196224
# the individual `val == values[i]` ValueError surface.
197225
exists = False
198226
if not exists:
199-
msg = f"{val!r} is not a valid value for {key}"
200-
if _print_supported_values:
201-
msg += f"; supported values are {', '.join(map(repr, values))}"
202-
raise ValueError(msg)
227+
raise ValueError(list_suggestion_error_msg(key, val, values))
203228

204229

205230
def check_shape(shape, /, **kwargs):
@@ -258,14 +283,7 @@ def getitem_checked(mapping, /, _error_cls=ValueError, **kwargs):
258283
try:
259284
return mapping[v]
260285
except KeyError:
261-
if len(mapping) > 5:
262-
if len(best := difflib.get_close_matches(v, mapping.keys(), cutoff=0.5)):
263-
suggestion = f"Did you mean one of {best}?"
264-
else:
265-
suggestion = ""
266-
else:
267-
suggestion = f"Supported values are {', '.join(map(repr, mapping))}"
268-
raise _error_cls(f"{v!r} is not a valid value for {k}. {suggestion}") from None
286+
raise _error_cls(list_suggestion_error_msg(k, v, mapping.keys())) from None
269287

270288

271289
def caching_module_getattr(cls):

lib/matplotlib/_api/__init__.pyi

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ class classproperty(Any):
3838
def check_isinstance(
3939
types: type | tuple[type | None, ...], /, **kwargs: Any
4040
) -> None: ...
41-
def check_in_list(
42-
values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any
43-
) -> None: ...
41+
def list_suggestion_error_msg(name: str, potential: Any, values: Sequence[Any]) -> str: ...
42+
def check_in_list(values: Sequence[Any], /, **kwargs: Any) -> None: ...
4443
def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ...
4544
def getitem_checked(
4645
mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any

lib/matplotlib/animation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def list(self):
117117

118118
def __getitem__(self, name):
119119
"""Get an available writer class from its name."""
120+
_api.check_in_list(self._registered, writer=name)
120121
if self.is_available(name):
121122
return self._registered[name]
122123
raise RuntimeError(f"Requested MovieWriter ({name}) not available")

lib/matplotlib/colorizer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,8 @@ def _ensure_cmap(cmap, accept_multivariate=False):
826826
# this error message is a variant of _api.check_in_list but gives
827827
# additional hints as to how to access multivariate colormaps
828828

829-
raise ValueError(f"{cmap!r} is not a valid value for cmap"
830-
"; supported values for scalar colormaps are "
831-
f"{', '.join(map(repr, sorted(mpl.colormaps)))}\n"
832-
"See `matplotlib.bivar_colormaps()` and"
829+
raise ValueError(_api.list_suggestion_error_msg('cmap', cmap, mpl.colormaps) +
830+
"\nSee `matplotlib.bivar_colormaps()` and"
833831
" `matplotlib.multivar_colormaps()` for"
834832
" bivariate and multivariate colormaps")
835833

lib/matplotlib/colors.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,8 @@ def __init__(self):
142142
self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES}
143143

144144
def __getitem__(self, item):
145-
try:
146-
return list(self._color_sequences[item])
147-
except KeyError:
148-
raise KeyError(f"{item!r} is not a known color sequence name")
145+
return list(_api.getitem_checked(self._color_sequences, _error_cls=KeyError,
146+
sequence_name=item))
149147

150148
def __iter__(self):
151149
return iter(self._color_sequences)

lib/matplotlib/projections/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
`matplotlib.projections.polar` may also be of interest.
5353
"""
5454

55-
from .. import axes, _docstring
55+
from .. import _api, axes, _docstring
5656
from .geo import AitoffAxes, HammerAxes, LambertAxes, MollweideAxes
5757
from .polar import PolarAxes
5858

@@ -78,9 +78,10 @@ def register(self, *projections):
7878
name = projection.name
7979
self._all_projection_types[name] = projection
8080

81-
def get_projection_class(self, name):
81+
def get_projection_class(self, name, _error_cls=KeyError):
8282
"""Get a projection class from its *name*."""
83-
return self._all_projection_types[name]
83+
return _api.getitem_checked(self._all_projection_types, _error_cls=_error_cls,
84+
projection=name)
8485

8586
def get_projection_names(self):
8687
"""Return the names of all projections currently registered."""
@@ -116,10 +117,7 @@ def get_projection_class(projection=None):
116117
if projection is None:
117118
projection = 'rectilinear'
118119

119-
try:
120-
return projection_registry.get_projection_class(projection)
121-
except KeyError as err:
122-
raise ValueError("Unknown projection %r" % projection) from err
120+
return projection_registry.get_projection_class(projection, _error_cls=ValueError)
123121

124122

125123
get_projection_names = projection_registry.get_projection_names

lib/matplotlib/tests/test_animation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ def animate(i):
4444
return klass(fig=fig, func=animate, init_func=init, **kwargs)
4545

4646

47+
def test_invalid_writer():
48+
# Note, this triggers for Animation.save as well, but this is a lighter test.
49+
with pytest.raises(ValueError,
50+
match=r"'pllow' is not a valid value for writer\. "
51+
r"Did you mean: 'pillow'\?"):
52+
animation.writers['pllow']
53+
54+
4755
class NullMovieWriter(animation.AbstractMovieWriter):
4856
"""
4957
A minimal MovieWriter. It doesn't actually write anything.

lib/matplotlib/tests/test_axes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3338,6 +3338,13 @@ def test_scatter_c_facecolor_warning_integration(c, facecolor):
33383338
ax.scatter(x, y, c=c, facecolor=facecolor)
33393339

33403340

3341+
def test_invalid_projection():
3342+
with pytest.raises(ValueError,
3343+
match=r"'aitof' is not a valid value for projection\. "
3344+
r"Did you mean: 'aitoff'\?"):
3345+
plt.subplots(subplot_kw={'projection': 'aitof'})
3346+
3347+
33413348
def test_as_mpl_axes_api():
33423349
# tests the _as_mpl_axes api
33433350
class Polar:

lib/matplotlib/tests/test_colors.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1882,10 +1882,15 @@ def test_close_error_name():
18821882
with pytest.raises(
18831883
KeyError,
18841884
match=(
1885-
"'grays' is not a valid value for colormap. "
1886-
"Did you mean one of ['gray', 'Grays', 'gray_r']?"
1887-
)):
1885+
r"'grays' is not a valid value for colormap\. "
1886+
r"Did you mean one of: 'gray', 'Grays', 'gray_r'\?")):
18881887
matplotlib.colormaps["grays"]
1888+
with pytest.raises(
1889+
KeyError,
1890+
match=(
1891+
"'set' is not a valid value for sequence_name. "
1892+
"Did you mean one of: 'Set3', 'Set2', 'Set1'?")):
1893+
matplotlib.color_sequences["set"]
18891894

18901895

18911896
def test_multi_norm_creation():

lib/matplotlib/tests/test_style.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,8 @@ 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(
154-
KeyError, match="\'badparam\' is not a valid value for rcParam. "
155-
):
153+
with pytest.raises(KeyError,
154+
match=r"'badparam' is not a valid value for rcParam\."):
156155
with x:
157156
pass
158157
assert mpl.rcParams[PARAM] == other_value

0 commit comments

Comments
 (0)