Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 431be66

Browse filesBrowse files
authored
Merge pull request #7097 from Kojoley/refactor-image_comparison-decorator
TST: `image_comparison` decorator refactor
2 parents 684861b + 909194b commit 431be66
Copy full SHA for 431be66

File tree

Expand file treeCollapse file tree

8 files changed

+219
-218
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+219
-218
lines changed

‎conftest.py

Copy file name to clipboardExpand all lines: conftest.py
-7Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
matplotlib.use('agg')
1111

1212
from matplotlib import default_test_modules
13-
from matplotlib.testing.decorators import ImageComparisonTest
1413

1514

1615
IGNORED_TESTS = {
@@ -86,12 +85,6 @@ def pytest_ignore_collect(path, config):
8685

8786
def pytest_pycollect_makeitem(collector, name, obj):
8887
if inspect.isclass(obj):
89-
if issubclass(obj, ImageComparisonTest):
90-
# Workaround `image_compare` decorator as it returns class
91-
# instead of function and this confuses pytest because it crawls
92-
# original names and sees 'test_*', but not 'Test*' in that case
93-
return pytest.Class(name, parent=collector)
94-
9588
if is_nose_class(obj) and not issubclass(obj, unittest.TestCase):
9689
# Workaround unittest-like setup/teardown names in pure classes
9790
setup = getattr(obj, 'setUp', None)

‎lib/matplotlib/testing/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/__init__.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def getrawcode(obj, trycall=True):
6262

6363
def copy_metadata(src_func, tgt_func):
6464
"""Replicates metadata of the function. Returns target function."""
65-
tgt_func.__dict__ = src_func.__dict__
65+
tgt_func.__dict__.update(src_func.__dict__)
6666
tgt_func.__doc__ = src_func.__doc__
6767
tgt_func.__module__ = src_func.__module__
6868
tgt_func.__name__ = src_func.__name__

‎lib/matplotlib/testing/decorators.py

Copy file name to clipboardExpand all lines: lib/matplotlib/testing/decorators.py
+168-118Lines changed: 168 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
from matplotlib import ticker
2525
from matplotlib import pyplot as plt
2626
from matplotlib import ft2font
27-
from matplotlib import rcParams
2827
from matplotlib.testing.compare import comparable_formats, compare_images, \
2928
make_test_filename
30-
from . import copy_metadata, is_called_from_pytest, skip, xfail
29+
from . import copy_metadata, is_called_from_pytest, xfail
3130
from .exceptions import ImageComparisonFailure
3231

3332

@@ -59,7 +58,10 @@ def knownfailureif(fail_condition, msg=None, known_exception_class=None):
5958
"""
6059
if is_called_from_pytest():
6160
import pytest
62-
strict = fail_condition and fail_condition != 'indeterminate'
61+
if fail_condition == 'indeterminate':
62+
fail_condition, strict = True, False
63+
else:
64+
fail_condition, strict = bool(fail_condition), True
6365
return pytest.mark.xfail(condition=fail_condition, reason=msg,
6466
raises=known_exception_class, strict=strict)
6567
else:
@@ -173,98 +175,171 @@ def check_freetype_version(ver):
173175
return found >= ver[0] and found <= ver[1]
174176

175177

176-
class ImageComparisonTest(CleanupTest):
177-
@classmethod
178-
def setup_class(cls):
179-
CleanupTest.setup_class()
178+
def checked_on_freetype_version(required_freetype_version):
179+
if check_freetype_version(required_freetype_version):
180+
return lambda f: f
181+
182+
reason = ("Mismatched version of freetype. "
183+
"Test requires '%s', you have '%s'" %
184+
(required_freetype_version, ft2font.__freetype_version__))
185+
return knownfailureif('indeterminate', msg=reason,
186+
known_exception_class=ImageComparisonFailure)
187+
188+
189+
def remove_ticks_and_titles(figure):
190+
figure.suptitle("")
191+
null_formatter = ticker.NullFormatter()
192+
for ax in figure.get_axes():
193+
ax.set_title("")
194+
ax.xaxis.set_major_formatter(null_formatter)
195+
ax.xaxis.set_minor_formatter(null_formatter)
196+
ax.yaxis.set_major_formatter(null_formatter)
197+
ax.yaxis.set_minor_formatter(null_formatter)
180198
try:
181-
matplotlib.style.use(cls._style)
199+
ax.zaxis.set_major_formatter(null_formatter)
200+
ax.zaxis.set_minor_formatter(null_formatter)
201+
except AttributeError:
202+
pass
203+
204+
205+
def raise_on_image_difference(expected, actual, tol):
206+
__tracebackhide__ = True
207+
208+
err = compare_images(expected, actual, tol, in_decorator=True)
209+
210+
if not os.path.exists(expected):
211+
raise ImageComparisonFailure('image does not exist: %s' % expected)
212+
213+
if err:
214+
raise ImageComparisonFailure(
215+
'images not close: %(actual)s vs. %(expected)s '
216+
'(RMS %(rms).3f)' % err)
217+
218+
219+
def xfail_if_format_is_uncomparable(extension):
220+
will_fail = extension not in comparable_formats()
221+
if will_fail:
222+
fail_msg = 'Cannot compare %s files on this system' % extension
223+
else:
224+
fail_msg = 'No failure expected'
225+
226+
return knownfailureif(will_fail, fail_msg,
227+
known_exception_class=ImageComparisonFailure)
228+
229+
230+
def mark_xfail_if_format_is_uncomparable(extension):
231+
will_fail = extension not in comparable_formats()
232+
if will_fail:
233+
fail_msg = 'Cannot compare %s files on this system' % extension
234+
import pytest
235+
return pytest.mark.xfail(extension, reason=fail_msg, strict=False,
236+
raises=ImageComparisonFailure)
237+
else:
238+
return extension
239+
240+
241+
class ImageComparisonDecorator(CleanupTest):
242+
def __init__(self, baseline_images, extensions, tol,
243+
freetype_version, remove_text, savefig_kwargs, style):
244+
self.func = self.baseline_dir = self.result_dir = None
245+
self.baseline_images = baseline_images
246+
self.extensions = extensions
247+
self.tol = tol
248+
self.freetype_version = freetype_version
249+
self.remove_text = remove_text
250+
self.savefig_kwargs = savefig_kwargs
251+
self.style = style
252+
253+
def setup(self):
254+
func = self.func
255+
self.setup_class()
256+
try:
257+
matplotlib.style.use(self.style)
182258
matplotlib.testing.set_font_settings_for_testing()
183-
cls._func()
259+
func()
260+
assert len(plt.get_fignums()) == len(self.baseline_images), (
261+
'Figures and baseline_images count are not the same'
262+
' (`%s`)' % getattr(func, '__qualname__', func.__name__))
184263
except:
185264
# Restore original settings before raising errors during the update.
186-
CleanupTest.teardown_class()
265+
self.teardown_class()
187266
raise
188267

189-
@classmethod
190-
def teardown_class(cls):
191-
CleanupTest.teardown_class()
192-
193-
@staticmethod
194-
def remove_text(figure):
195-
figure.suptitle("")
196-
for ax in figure.get_axes():
197-
ax.set_title("")
198-
ax.xaxis.set_major_formatter(ticker.NullFormatter())
199-
ax.xaxis.set_minor_formatter(ticker.NullFormatter())
200-
ax.yaxis.set_major_formatter(ticker.NullFormatter())
201-
ax.yaxis.set_minor_formatter(ticker.NullFormatter())
202-
try:
203-
ax.zaxis.set_major_formatter(ticker.NullFormatter())
204-
ax.zaxis.set_minor_formatter(ticker.NullFormatter())
205-
except AttributeError:
206-
pass
268+
def teardown(self):
269+
self.teardown_class()
270+
271+
def copy_baseline(self, baseline, extension):
272+
baseline_path = os.path.join(self.baseline_dir, baseline)
273+
orig_expected_fname = baseline_path + '.' + extension
274+
if extension == 'eps' and not os.path.exists(orig_expected_fname):
275+
orig_expected_fname = baseline_path + '.pdf'
276+
expected_fname = make_test_filename(os.path.join(
277+
self.result_dir, os.path.basename(orig_expected_fname)), 'expected')
278+
actual_fname = os.path.join(self.result_dir, baseline) + '.' + extension
279+
if os.path.exists(orig_expected_fname):
280+
shutil.copyfile(orig_expected_fname, expected_fname)
281+
else:
282+
xfail("Do not have baseline image {0} because this "
283+
"file does not exist: {1}".format(expected_fname,
284+
orig_expected_fname))
285+
return expected_fname, actual_fname
286+
287+
def compare(self, idx, baseline, extension):
288+
__tracebackhide__ = True
289+
if self.baseline_dir is None:
290+
self.baseline_dir, self.result_dir = _image_directories(self.func)
291+
expected_fname, actual_fname = self.copy_baseline(baseline, extension)
292+
fignum = plt.get_fignums()[idx]
293+
fig = plt.figure(fignum)
294+
if self.remove_text:
295+
remove_ticks_and_titles(fig)
296+
fig.savefig(actual_fname, **self.savefig_kwargs)
297+
raise_on_image_difference(expected_fname, actual_fname, self.tol)
298+
299+
def nose_runner(self):
300+
func = self.compare
301+
func = checked_on_freetype_version(self.freetype_version)(func)
302+
funcs = {extension: xfail_if_format_is_uncomparable(extension)(func)
303+
for extension in self.extensions}
304+
for idx, baseline in enumerate(self.baseline_images):
305+
for extension in self.extensions:
306+
yield funcs[extension], idx, baseline, extension
307+
308+
def pytest_runner(self):
309+
from pytest import mark
310+
311+
extensions = map(mark_xfail_if_format_is_uncomparable, self.extensions)
312+
313+
@mark.parametrize("extension", extensions)
314+
@mark.parametrize("idx,baseline", enumerate(self.baseline_images))
315+
@checked_on_freetype_version(self.freetype_version)
316+
def wrapper(idx, baseline, extension):
317+
__tracebackhide__ = True
318+
self.compare(idx, baseline, extension)
319+
320+
# sadly we cannot use fixture here because of visibility problems
321+
# and for for obvious reason avoid `nose.tools.with_setup`
322+
wrapper.setup, wrapper.teardown = self.setup, self.teardown
323+
324+
return wrapper
325+
326+
def __call__(self, func):
327+
self.func = func
328+
if is_called_from_pytest():
329+
return copy_metadata(func, self.pytest_runner())
330+
else:
331+
import nose.tools
207332

208-
def test(self):
209-
baseline_dir, result_dir = _image_directories(self._func)
210-
211-
for fignum, baseline in zip(plt.get_fignums(), self._baseline_images):
212-
for extension in self._extensions:
213-
will_fail = not extension in comparable_formats()
214-
if will_fail:
215-
fail_msg = 'Cannot compare %s files on this system' % extension
216-
else:
217-
fail_msg = 'No failure expected'
218-
219-
orig_expected_fname = os.path.join(baseline_dir, baseline) + '.' + extension
220-
if extension == 'eps' and not os.path.exists(orig_expected_fname):
221-
orig_expected_fname = os.path.join(baseline_dir, baseline) + '.pdf'
222-
expected_fname = make_test_filename(os.path.join(
223-
result_dir, os.path.basename(orig_expected_fname)), 'expected')
224-
actual_fname = os.path.join(result_dir, baseline) + '.' + extension
225-
if os.path.exists(orig_expected_fname):
226-
shutil.copyfile(orig_expected_fname, expected_fname)
227-
else:
228-
will_fail = True
229-
fail_msg = (
230-
"Do not have baseline image {0} because this "
231-
"file does not exist: {1}".format(
232-
expected_fname,
233-
orig_expected_fname
234-
)
235-
)
236-
237-
@knownfailureif(
238-
will_fail, fail_msg,
239-
known_exception_class=ImageComparisonFailure)
240-
def do_test(fignum, actual_fname, expected_fname):
241-
figure = plt.figure(fignum)
242-
243-
if self._remove_text:
244-
self.remove_text(figure)
245-
246-
figure.savefig(actual_fname, **self._savefig_kwarg)
247-
248-
err = compare_images(expected_fname, actual_fname,
249-
self._tol, in_decorator=True)
250-
251-
try:
252-
if not os.path.exists(expected_fname):
253-
raise ImageComparisonFailure(
254-
'image does not exist: %s' % expected_fname)
255-
256-
if err:
257-
raise ImageComparisonFailure(
258-
'images not close: %(actual)s vs. %(expected)s '
259-
'(RMS %(rms).3f)'%err)
260-
except ImageComparisonFailure:
261-
if not check_freetype_version(self._freetype_version):
262-
xfail(
263-
"Mismatched version of freetype. Test requires '%s', you have '%s'" %
264-
(self._freetype_version, ft2font.__freetype_version__))
265-
raise
266-
267-
yield do_test, fignum, actual_fname, expected_fname
333+
@nose.tools.with_setup(self.setup, self.teardown)
334+
def runner_wrapper():
335+
try:
336+
for case in self.nose_runner():
337+
yield case
338+
except GeneratorExit:
339+
# nose bug...
340+
self.teardown()
341+
342+
return copy_metadata(func, runner_wrapper)
268343

269344

270345
def image_comparison(baseline_images=None, extensions=None, tol=0,
@@ -323,35 +398,11 @@ def image_comparison(baseline_images=None, extensions=None, tol=0,
323398
#default no kwargs to savefig
324399
savefig_kwarg = dict()
325400

326-
def compare_images_decorator(func):
327-
# We want to run the setup function (the actual test function
328-
# that generates the figure objects) only once for each type
329-
# of output file. The only way to achieve this with nose
330-
# appears to be to create a test class with "setup_class" and
331-
# "teardown_class" methods. Creating a class instance doesn't
332-
# work, so we use type() to actually create a class and fill
333-
# it with the appropriate methods.
334-
name = func.__name__
335-
# For nose 1.0, we need to rename the test function to
336-
# something without the word "test", or it will be run as
337-
# well, outside of the context of our image comparison test
338-
# generator.
339-
func = staticmethod(func)
340-
func.__get__(1).__name__ = str('_private')
341-
new_class = type(
342-
name,
343-
(ImageComparisonTest,),
344-
{'_func': func,
345-
'_baseline_images': baseline_images,
346-
'_extensions': extensions,
347-
'_tol': tol,
348-
'_freetype_version': freetype_version,
349-
'_remove_text': remove_text,
350-
'_savefig_kwarg': savefig_kwarg,
351-
'_style': style})
352-
353-
return new_class
354-
return compare_images_decorator
401+
return ImageComparisonDecorator(
402+
baseline_images=baseline_images, extensions=extensions, tol=tol,
403+
freetype_version=freetype_version, remove_text=remove_text,
404+
savefig_kwargs=savefig_kwarg, style=style)
405+
355406

356407
def _image_directories(func):
357408
"""
@@ -416,7 +467,6 @@ def find_dotted_module(module_name, path=None):
416467
def switch_backend(backend):
417468
# Local import to avoid a hard nose dependency and only incur the
418469
# import time overhead at actual test-time.
419-
import nose
420470
def switch_backend_decorator(func):
421471
def backend_switcher(*args, **kwargs):
422472
try:
Loading

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.