-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Make image_comparison more pytest-y #8380
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
Make image_comparison more pytest-y #8380
Conversation
cd02a49
to
0913080
Compare
This is impressive work! It's also a bit over my head. Would these changes render pytest-mpl obsolete, or does the |
I did not really set out to do so, so I can't say that definitively. And realistically, our decorator had mostly (modulo the minor issues fixed here) always worked and probably will continue to work just fine. It's really a matter of whether we want to continue developing it or not. The change does look a bit bigger than it really is; what it boils down to is splitting one class that does everything into one class that does just comparison (so some stuff got moved up), one class that works just with nose (some stuff got moved down), and one wrapper function that works just with pytest (a bunch of stuff got deleted). And then tweaking the original decorator so it calls the right one. |
Well, pytest-mpl uses our What we need to be careful about is making sure that this doesn't break pytest-mpl. |
It looks like |
Wow, that was really hard to find. #7097 |
I added a rename back to the old name; I don't think it should be a problem, but we don't use it anymore, so it might be useful to try it out. |
Instead of a heavy do-it-all class, split ImageComparisonDecorator into a smaller class that does just the comparison stuff and one that does only nose. For pytest, use a wrapper function that's decorated only by pytest decorators, and don't try to modify the function signature. By using a separate fixture, we can indirectly return the parameterized arguments instead. This stops pytest from getting confused about what takes what argument. The biggest benefit is that test code is now run as the *test*, whereas previously, it was run as the *setup* causing it to have all sorts of semantic irregularities.
The main point is it can be indirectly determined from other parametrizations.
A similar check is done in calculate_rms.
While this does cause a periodic printout if using the image_comparison decorator from nose, trying to handle this exception means that test images don't get checked at all, which is even worse.
This should ensure that it continues to work for downstream users even though we've stopped using it.
0ab7f4b
to
ba7106a
Compare
I've updated this with:
|
Looks like even indirectly, we don't install |
# pytest won't get confused. | ||
# We annotate the decorated function with any parameters captured by this | ||
# fixture so that they can be used by the wrapper in image_comparison. | ||
baseline_images = request.keywords['baseline_images'].args[0] |
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.
Looks like from a quick skim through the codebase that baseline_images is always a list with a single element.
- Should we switch to having a single
baseline_image
(singular)? Perhaps the ability to compare multiple images is useful, though. - If we want to assert that baseline_images is always of length 1, I prefer
baseline_image, = request.keywords["baseline_images"].args
which will error when multiple values are passed.
If we don't, then we shouldn't silently drop extra values here.
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.
The [0]
refers to the fact that I passed the original baseline_images
as the first argument of the marker. There are multiple tests that do use more than one baseline image.
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.
So request.keywords['baseline_images']
contains all args, nor just baseline_images? If so that's probably worth a comment.
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.
request.keywords['baseline_images']
contains the pytest marker, .args
are the arguments passed to the marker, .kwargs
are keyword arguments passed to the marker, etc.
Perhaps I should have given the marker a different name...
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.
Ah, I see.
lib/matplotlib/testing/conftest.py
Outdated
|
||
func = request.function | ||
func.__wrapped__.parameters = (baseline_images, extension) | ||
yield |
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 think the typical pattern is try: yield finally: ... (even though it probably doesn't matter here)
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.
The one above uses it, so I'll change this one too to be consistent.
The naming is slightly confusing. If I understand correctly, _ImageComparisonBase is the class now used by pytest and ImageComparisonTest (which inherits it) is used by nose. If so, perhaps rename the first one to ImageComparison and the second to NoseImageComparison? Looks reasonable enough to install nose to test nose compatibility. |
Can't change the name of the nose one for backwards compatibility. I don't really want to make |
That's reasonable, but can you leave comments to that effect then? Again it took me a while to understand the reason of the split. |
Added some comments and handled some of the other stuff you mentioned. |
lib/matplotlib/testing/decorators.py
Outdated
Nose-based image comparison class | ||
|
||
This class generates tests for a nose-based testing framework. Ideally, | ||
this class would not be public, and the only publically visibile API would |
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.
typo: visible.
lib/matplotlib/testing/decorators.py
Outdated
This function creates a decorator that wraps a figure-generating function | ||
with image comparison code. Pytest can become confused if we change the | ||
signature of the function, so we indirectly pass anything we need via the | ||
``mpl_image_comparison_parameters`` fixture and extra markers. |
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.
single backquotes (it's a reference)
LGTM modulo typo and conditional on tests passing as usual :-) |
* Install nose in one build. * Add docstrings and comments on new functions/classes to clarify what they do. * Use consistent `yield` pattern for fixture.
8fc25dc
to
d1c1e1b
Compare
Even codecov is happy! |
I'm a bit tired of
image_comparison
breaking pytest subtly. It was written in a way that mirrorsnose
with the test as "setup" and the "test" is just saving and comparing the results. This has a number of disadvantages.However, switching to a decorated function had a problem; we couldn't change the signature of the decorated function or pytest would get confused about where to send the fixtures. So we couldn't, for example, parametrize based on extension. The trick I've found is that the test doesn't need to take a fixture if one of its fixtures does. So I hacked this together by having the fixture send the parameters back to the wrapper via the decorated function. Now,
image_comparison
is simply another decorator as far a pytest is concerned (because it doesn't modify the function signature.)This method has a number of advantages:
monkeypatch
or something else as an argument.)baseline_images
as a possible fixture, but this could be extended. This can be seen being used in the mathtext tests in the last commit.There's only one obvious (to me) disadvantage: