-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Bugfix for loc legend validation #25281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bugfix for loc legend validation #25281
Conversation
@robrighter in your original post, if you replace “addresses the issue #24605” with just “resolves #24605”, it will link the issue so it automatically closes when this is merged. You could also link to the old PR in the same way so that closes too. There are various keywords that work: |
lib/matplotlib/legend.py
Outdated
if len(loc) != 2 or not (all(isinstance(e, numbers.Real) | ||
for e in loc)): | ||
raise ValueError( | ||
f"loc must be string or pair of numbers, not {loc!r}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this include a note about [0, 9] being allowed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but I'm unsure about the wording. Maybe "loc must be string, coordinate tuple or, an integer 0-10, not xyz"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for bonus points you can also include the list of allowed strings!
One of the goals of our error messages should be, if it makes sense, to give the user enough of a hint about what they did wrong so that they can sort out how to fix it just from reading the traceback (and not have to go open a browser to search for the docs).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This thought turned into #25292
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the pull request to be more verbose. In your error message guidelines doc you mentioned that we should try to use the helper methods to standardize the error messages. I think that is good advice but it limits the ability to customize the error message to fit the context. To strike the balance in this situation, my suggestion is:
- Use a less verbose error message when the user provides a non-string type such as " "loc must be string, coordinate tuple or, an integer 0-10, not 21"
- Use the built in (and more verbose) error message when the user attempts to provide a string that does not match the allowed strings. This uses _api.check_getitem and yields a message like: "'xyz' is not a valid value for loc; supported values are 'best', 'upper right', 'upper left', 'lower left', 'lower right', 'right', 'center left', 'center right', 'lower center', 'upper center', 'center'"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tacaswell let me know if this tradeoff works for you or if you think I should update all the responses to include all the information. Either works for me. ^
385387b
to
5a30ae7
Compare
@tacaswell are there any additional changes you would like to see on this PR? |
6d3027d
to
51385ed
Compare
23f5667
to
0634c0a
Compare
lib/matplotlib/legend.py
Outdated
@@ -535,6 +539,17 @@ def val_or_rc(val, rc_name): | |||
loc = locs[0] + ' ' + locs[1] | |||
# check that loc is in acceptable strings | |||
loc = _api.check_getitem(self.codes, loc=loc) | |||
elif isinstance(loc, tuple): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should be more lenient here: currently there is nothing to stop a user passing e.g. a 2-element list, and it works just fine, but this will presumably break anyone doing so. Not long ago we had a similar discussion about the iterable passed to colorbar
, and ultimately decided to be more lenient than had previously been documented #24408.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems reasonable to me. If we allow a list we should:
-
check the length and error on the wrong length.
-
update the docs and make it a supported type.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For maximum flexibility, we could attempt to convert anything that isn't str
or int
into a tuple within a try-except loop. Then just use your existing handling for tuple. Probably better to wait and see what other core developers think though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead an added the case for lists and then convert the list into a tuple. The tuple logic then evaluates the length, etc.
I changed my mind on the documentation. It seems like we are only allowing lists for backward compatibility. Tuples are probably the better choice and so the api should probably advertise tuples as the valid type for coordinate pairs.
1684278
to
379fc6c
Compare
lib/matplotlib/legend.py
Outdated
# handle outside legends: | ||
self._outside_loc = None | ||
|
||
# coerce list into tuple for backward compatibility | ||
if isinstance(loc, list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if isinstance(loc, list): | |
if isinstance(loc, collections.abc.Iterable): |
This would make it more general. A list is the most obvious type of iterable someone might pass, but there may be others. Your decision on the documentation makes sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We seem to rely on np.iterable
for this type of checks.
(It should probably be collections.abc.Iterable
as well as importing directly is removed in 3.11.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, we don’t want to convert all iterables here, because that would include strings and we clearly don’t want them to become tuples! So I think this should come after the string handling…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
lib/matplotlib/legend.py
Outdated
# handle outside legends: | ||
self._outside_loc = None | ||
|
||
# coerce list into tuple for backward compatibility | ||
if isinstance(loc, list): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait, we don’t want to convert all iterables here, because that would include strings and we clearly don’t want them to become tuples! So I think this should come after the string handling…
lib/matplotlib/legend.py
Outdated
@@ -535,6 +544,17 @@ def val_or_rc(val, rc_name): | |||
loc = locs[0] + ' ' + locs[1] | |||
# check that loc is in acceptable strings | |||
loc = _api.check_getitem(self.codes, loc=loc) | |||
elif isinstance(loc, tuple): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
elif isinstance(loc, tuple): | |
elif np.iterable(loc) | |
loc = tuple(loc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
379fc6c
to
586e585
Compare
Co-authored-by: John Paul Jepko <jpjepko@users.noreply.github.com>
586e585
to
f263f7d
Compare
Thanks @robrighter! |
Co-authored-by: John Paul Jepko jpjepko@users.noreply.github.com
PR Summary
This pull request adds a check to the loc argument of legend. It checks that loc is one of the following:
The code raises a ValueError otherwise. This resolves #24605 . The docstrings already mentioned the fact that loc is a pair of floats so we did not add any changes to any docstrings. We also added test cases for the new validation checks. For details on the valid options refer to this doc
PR Checklist
Documentation and Tests
pytest
passes)Release Notes
.. versionadded::
directive in the docstring and documented indoc/users/next_whats_new/
.. versionchanged::
directive in the docstring and documented indoc/api/next_api_changes/
next_whats_new/README.rst
ornext_api_changes/README.rst