Skip to content

Commit abc9445

Browse files
committed
TST: Make image_comparison more pytest-y.
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.
1 parent 14b5d17 commit abc9445

File tree

5 files changed

+105
-68
lines changed

5 files changed

+105
-68
lines changed

lib/matplotlib/sphinxext/tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
unicode_literals)
33

44
from matplotlib.testing.conftest import (mpl_test_settings,
5+
mpl_image_comparison_parameters,
56
pytest_configure, pytest_unconfigure)

lib/matplotlib/testing/conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,19 @@ def mpl_test_settings(request):
5353
plt.switch_backend(prev_backend)
5454
_do_cleanup(original_units_registry,
5555
original_settings)
56+
57+
58+
@pytest.fixture
59+
def mpl_image_comparison_parameters(request, extension):
60+
# This fixture is applied automatically by the image_comparison decorator.
61+
#
62+
# The sole purpose of this fixture is to provide an indirect method of
63+
# obtaining parameters *without* modifying the decorated function
64+
# signature. In this way, the function signature can stay the same and
65+
# pytest won't get confused.
66+
# We annotate the decorated function with any parameters captured by this
67+
# fixture so that they can be used by the wrapper in image_comparison.
68+
func = request.function
69+
func.__wrapped__.parameters = (extension, )
70+
yield
71+
delattr(func.__wrapped__, 'parameters')

lib/matplotlib/testing/decorators.py

+86-68
Original file line numberDiff line numberDiff line change
@@ -233,42 +233,18 @@ def _mark_xfail_if_format_is_uncomparable(extension):
233233
return extension
234234

235235

236-
class ImageComparisonDecorator(CleanupTest):
237-
def __init__(self, baseline_images, extensions, tol,
238-
freetype_version, remove_text, savefig_kwargs, style):
236+
class _ImageComparisonBase(object):
237+
def __init__(self, tol, remove_text, savefig_kwargs):
239238
self.func = self.baseline_dir = self.result_dir = None
240-
self.baseline_images = baseline_images
241-
self.extensions = extensions
242239
self.tol = tol
243-
self.freetype_version = freetype_version
244240
self.remove_text = remove_text
245241
self.savefig_kwargs = savefig_kwargs
246-
self.style = style
247242

248243
def delayed_init(self, func):
249244
assert self.func is None, "it looks like same decorator used twice"
250245
self.func = func
251246
self.baseline_dir, self.result_dir = _image_directories(func)
252247

253-
def setup(self):
254-
func = self.func
255-
plt.close('all')
256-
self.setup_class()
257-
try:
258-
matplotlib.style.use(self.style)
259-
matplotlib.testing.set_font_settings_for_testing()
260-
func()
261-
assert len(plt.get_fignums()) == len(self.baseline_images), (
262-
"Test generated {} images but there are {} baseline images"
263-
.format(len(plt.get_fignums()), len(self.baseline_images)))
264-
except:
265-
# Restore original settings before raising errors during the update.
266-
self.teardown_class()
267-
raise
268-
269-
def teardown(self):
270-
self.teardown_class()
271-
272248
def copy_baseline(self, baseline, extension):
273249
baseline_path = os.path.join(self.baseline_dir, baseline)
274250
orig_expected_fname = baseline_path + '.' + extension
@@ -304,6 +280,35 @@ def compare(self, idx, baseline, extension):
304280
expected_fname = self.copy_baseline(baseline, extension)
305281
_raise_on_image_difference(expected_fname, actual_fname, self.tol)
306282

283+
284+
class ImageComparisonDecorator(CleanupTest, _ImageComparisonBase):
285+
def __init__(self, baseline_images, extensions, tol,
286+
freetype_version, remove_text, savefig_kwargs, style):
287+
_ImageComparisonBase.__init__(self, tol, remove_text, savefig_kwargs)
288+
self.baseline_images = baseline_images
289+
self.extensions = extensions
290+
self.freetype_version = freetype_version
291+
self.style = style
292+
293+
def setup(self):
294+
func = self.func
295+
plt.close('all')
296+
self.setup_class()
297+
try:
298+
matplotlib.style.use(self.style)
299+
matplotlib.testing.set_font_settings_for_testing()
300+
func()
301+
assert len(plt.get_fignums()) == len(self.baseline_images), (
302+
"Test generated {} images but there are {} baseline images"
303+
.format(len(plt.get_fignums()), len(self.baseline_images)))
304+
except:
305+
# Restore original settings before raising errors.
306+
self.teardown_class()
307+
raise
308+
309+
def teardown(self):
310+
self.teardown_class()
311+
307312
def nose_runner(self):
308313
func = self.compare
309314
func = _checked_on_freetype_version(self.freetype_version)(func)
@@ -313,52 +318,59 @@ def nose_runner(self):
313318
for extension in self.extensions:
314319
yield funcs[extension], idx, baseline, extension
315320

316-
def pytest_runner(self):
317-
from pytest import mark
321+
def __call__(self, func):
322+
self.delayed_init(func)
323+
import nose.tools
318324

319-
extensions = map(_mark_xfail_if_format_is_uncomparable,
320-
self.extensions)
325+
@nose.tools.with_setup(self.setup, self.teardown)
326+
def runner_wrapper():
327+
try:
328+
for case in self.nose_runner():
329+
yield case
330+
except GeneratorExit:
331+
# nose bug...
332+
self.teardown()
321333

322-
if len(set(self.baseline_images)) == len(self.baseline_images):
323-
@mark.parametrize("extension", extensions)
324-
@mark.parametrize("idx,baseline", enumerate(self.baseline_images))
325-
@_checked_on_freetype_version(self.freetype_version)
326-
def wrapper(idx, baseline, extension):
327-
__tracebackhide__ = True
328-
self.compare(idx, baseline, extension)
329-
else:
330-
# Some baseline images are repeated, so run this in serial.
331-
@mark.parametrize("extension", extensions)
332-
@_checked_on_freetype_version(self.freetype_version)
333-
def wrapper(extension):
334-
__tracebackhide__ = True
335-
for idx, baseline in enumerate(self.baseline_images):
336-
self.compare(idx, baseline, extension)
334+
return _copy_metadata(func, runner_wrapper)
337335

338336

339-
# sadly we cannot use fixture here because of visibility problems
340-
# and for for obvious reason avoid `_nose.tools.with_setup`
341-
wrapper.setup, wrapper.teardown = self.setup, self.teardown
337+
def _pytest_image_comparison(baseline_images, extensions, tol,
338+
freetype_version, remove_text, savefig_kwargs,
339+
style):
340+
import pytest
342341

343-
return wrapper
342+
extensions = map(_mark_xfail_if_format_is_uncomparable, extensions)
344343

345-
def __call__(self, func):
346-
self.delayed_init(func)
347-
if is_called_from_pytest():
348-
return _copy_metadata(func, self.pytest_runner())
349-
else:
350-
import nose.tools
344+
def decorator(func):
345+
@pytest.mark.usefixtures('mpl_image_comparison_parameters')
346+
@pytest.mark.parametrize('extension', extensions)
347+
@pytest.mark.style(style)
348+
@_checked_on_freetype_version(freetype_version)
349+
@functools.wraps(func)
350+
def wrapper(*args, **kwargs):
351+
__tracebackhide__ = True
352+
img = _ImageComparisonBase(tol=tol, remove_text=remove_text,
353+
savefig_kwargs=savefig_kwargs)
354+
img.delayed_init(func)
355+
matplotlib.testing.set_font_settings_for_testing()
356+
func(*args, **kwargs)
351357

352-
@nose.tools.with_setup(self.setup, self.teardown)
353-
def runner_wrapper():
354-
try:
355-
for case in self.nose_runner():
356-
yield case
357-
except GeneratorExit:
358-
# nose bug...
359-
self.teardown()
358+
# This is hacked on via the mpl_image_comparison_parameters fixture
359+
# so that we don't need to modify the function's real signature for
360+
# any parametrization. Modifying the signature is very very tricky
361+
# and likely to confuse pytest.
362+
extension, = func.parameters
363+
364+
assert len(plt.get_fignums()) == len(baseline_images), (
365+
"Test generated {} images but there are {} baseline images"
366+
.format(len(plt.get_fignums()), len(baseline_images)))
367+
for idx, baseline in enumerate(baseline_images):
368+
img.compare(idx, baseline, extension)
369+
370+
wrapper.__wrapped__ = func # For Python 2.7.
371+
return _copy_metadata(func, wrapper)
360372

361-
return _copy_metadata(func, runner_wrapper)
373+
return decorator
362374

363375

364376
def image_comparison(baseline_images=None, extensions=None, tol=0,
@@ -414,10 +426,16 @@ def image_comparison(baseline_images=None, extensions=None, tol=0,
414426
#default no kwargs to savefig
415427
savefig_kwarg = dict()
416428

417-
return ImageComparisonDecorator(
418-
baseline_images=baseline_images, extensions=extensions, tol=tol,
419-
freetype_version=freetype_version, remove_text=remove_text,
420-
savefig_kwargs=savefig_kwarg, style=style)
429+
if is_called_from_pytest():
430+
return _pytest_image_comparison(
431+
baseline_images=baseline_images, extensions=extensions, tol=tol,
432+
freetype_version=freetype_version, remove_text=remove_text,
433+
savefig_kwargs=savefig_kwarg, style=style)
434+
else:
435+
return ImageComparisonDecorator(
436+
baseline_images=baseline_images, extensions=extensions, tol=tol,
437+
freetype_version=freetype_version, remove_text=remove_text,
438+
savefig_kwargs=savefig_kwarg, style=style)
421439

422440

423441
def _image_directories(func):

lib/matplotlib/tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
unicode_literals)
33

44
from matplotlib.testing.conftest import (mpl_test_settings,
5+
mpl_image_comparison_parameters,
56
pytest_configure, pytest_unconfigure)

lib/mpl_toolkits/tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
unicode_literals)
33

44
from matplotlib.testing.conftest import (mpl_test_settings,
5+
mpl_image_comparison_parameters,
56
pytest_configure, pytest_unconfigure)

0 commit comments

Comments
 (0)