Skip to content

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

Merged
merged 10 commits into from
Apr 16, 2017

Conversation

QuLogic
Copy link
Member

@QuLogic QuLogic commented Mar 26, 2017

I'm a bit tired of image_comparison breaking pytest subtly. It was written in a way that mirrors nose 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:

  • Test code is actually run as the test phase, not the setup phase. This makes more sense semantically:
  • No signature changes means we can use any fixture we want on an image test. (Marks already worked, but now we can ask for, e.g., monkeypatch or something else as an argument.)
  • Image comparisons can be parametrized. At the moment, I've only setup 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:

  • Previously, the test phase was just comparisons, so we could parametrize this by baseline image name. Now with the test phase as figure creation and comparison, we can't parametrize on image name. Since we already parametrize on extension, I think this is a small price to pay.

@QuLogic QuLogic added this to the 2.1 (next point release) milestone Mar 26, 2017
@QuLogic QuLogic force-pushed the pytest-image_comparison branch from cd02a49 to 0913080 Compare March 27, 2017 03:53
@phobson
Copy link
Member

phobson commented Mar 28, 2017

This is impressive work! It's also a bit over my head.

Would these changes render pytest-mpl obsolete, or does the image_comparison remain pretty low-level under this code?

@QuLogic
Copy link
Member Author

QuLogic commented Mar 29, 2017

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.

@dopplershift
Copy link
Contributor

Well, pytest-mpl uses our image_comparison code, so it's won't obsolete it. At the very least, pytest-mpl provides an actual pytest plugin, with things like extra command-line arguments.

What we need to be careful about is making sure that this doesn't break pytest-mpl.

@QuLogic
Copy link
Member Author

QuLogic commented Mar 29, 2017

It looks like pytest-mpl uses ImageComparisonTest, which was already renamed on master at some point (not by me, so I don't know when.) I think this PR is actually useful for pytest-mpl because it splits the comparison part from the decorator part (which it doesn't use at all.) I tried to keep the external API the same from before this PR, if you don't feel like changing it (but as I said, it's already been broken.)

@dopplershift
Copy link
Contributor

Wow, that was really hard to find. #7097

@QuLogic
Copy link
Member Author

QuLogic commented Mar 30, 2017

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.

QuLogic added 8 commits April 15, 2017 02:45
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.
@QuLogic QuLogic force-pushed the pytest-image_comparison branch from 0ab7f4b to ba7106a Compare April 15, 2017 06:47
@QuLogic
Copy link
Member Author

QuLogic commented Apr 15, 2017

I've updated this with:

  1. a rebase,
  2. restoring ImageComparisonTest and ImageComparisonTest.remove_text which should allow pytest-mpl to continue working (@dopplershift: It would be nice if pytest-mpl used the remove_ticks_and_titles function directly, though).
  3. a test of the nose-version of image_comparison, which should ensure it continues to work for downstream users (assuming I've got the test written up correctly.)

@QuLogic
Copy link
Member Author

QuLogic commented Apr 15, 2017

Looks like even indirectly, we don't install nose; should I install it for this one test?

# 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]
Copy link
Contributor

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.

  1. Should we switch to having a single baseline_image (singular)? Perhaps the ability to compare multiple images is useful, though.
  2. 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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

@QuLogic QuLogic Apr 15, 2017

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, .argsare 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...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see.


func = request.function
func.__wrapped__.parameters = (baseline_images, extension)
yield
Copy link
Contributor

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)

Copy link
Member Author

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.

@anntzer
Copy link
Contributor

anntzer commented Apr 15, 2017

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.

@QuLogic
Copy link
Member Author

QuLogic commented Apr 15, 2017

Can't change the name of the nose one for backwards compatibility. I don't really want to make _ImageComparisonBase public for the same reason.

@anntzer
Copy link
Contributor

anntzer commented Apr 15, 2017

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.

@QuLogic
Copy link
Member Author

QuLogic commented Apr 15, 2017

Added some comments and handled some of the other stuff you mentioned.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: visible.

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.
Copy link
Contributor

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)

@anntzer
Copy link
Contributor

anntzer commented Apr 15, 2017

LGTM modulo typo and conditional on tests passing as usual :-)

@anntzer anntzer changed the title Make image_comparison more pytest-y [MRG+1] Make image_comparison more pytest-y Apr 15, 2017
QuLogic added 2 commits April 15, 2017 18:48
* Install nose in one build.
* Add docstrings and comments on new functions/classes to clarify what
  they do.
* Use consistent `yield` pattern for fixture.
@QuLogic QuLogic force-pushed the pytest-image_comparison branch from 8fc25dc to d1c1e1b Compare April 15, 2017 22:49
@QuLogic
Copy link
Member Author

QuLogic commented Apr 16, 2017

Even codecov is happy!

@tacaswell tacaswell merged commit 9bcadae into matplotlib:master Apr 16, 2017
@QuLogic QuLogic deleted the pytest-image_comparison branch April 17, 2017 05:01
@QuLogic QuLogic changed the title [MRG+1] Make image_comparison more pytest-y Make image_comparison more pytest-y Apr 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants