Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 431be66

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

8 files changed

Lines changed: 219 additions & 218 deletions

File tree

conftest.py

Lines 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

Lines 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

Lines 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:
64 Bytes
Loading

0 commit comments

Comments
 (0)