From 1b24a164c87858d6d3f170662e419f9f54719830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 7 Jan 2017 17:58:40 +0200 Subject: [PATCH 01/19] Factor the converted-image cache out of compare.py There is a cache of png files keyed by the MD5 hashes of corresponding svg and pdf files, which helps reduce test suite running times for svg and pdf files that stay exactly the same from one run to the next. This patch enables caching of test results, not only expected results, which is only useful if the tests are mostly deterministic (see #7748). It adds reporting of cache misses, which can be helpful in getting tests to stay deterministic, and expiration since the test results are going to change more often than the expected results. --- lib/matplotlib/testing/_nose/__init__.py | 3 +- lib/matplotlib/testing/compare.py | 85 ++------ lib/matplotlib/testing/conftest.py | 28 +++ lib/matplotlib/testing/conversion_cache.py | 196 ++++++++++++++++++ lib/matplotlib/testing/decorators.py | 4 +- .../testing/nose/plugins/conversion_cache.py | 44 ++++ lib/matplotlib/tests/test_cache.py | 125 +++++++++++ 7 files changed, 417 insertions(+), 68 deletions(-) create mode 100644 lib/matplotlib/testing/conversion_cache.py create mode 100644 lib/matplotlib/testing/nose/plugins/conversion_cache.py create mode 100644 lib/matplotlib/tests/test_cache.py diff --git a/lib/matplotlib/testing/_nose/__init__.py b/lib/matplotlib/testing/_nose/__init__.py index d513c7b14f4b..2f64f440dc7e 100644 --- a/lib/matplotlib/testing/_nose/__init__.py +++ b/lib/matplotlib/testing/_nose/__init__.py @@ -7,9 +7,10 @@ def get_extra_test_plugins(): from .plugins.performgc import PerformGC from .plugins.knownfailure import KnownFailure + from .plugins.conversion_cache import ConversionCache from nose.plugins import attrib - return [PerformGC, KnownFailure, attrib.Plugin] + return [PerformGC, KnownFailure, attrib.Plugin, ConversionCache] def get_env(): diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index ab2f26fc0081..b2d77e7907e2 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -5,11 +5,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -import six - -import hashlib import os -import shutil import numpy as np @@ -17,9 +13,6 @@ from matplotlib.compat import subprocess from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png -from matplotlib import _get_cachedir -from matplotlib import cbook -from distutils import version __all__ = ['compare_float', 'compare_images', 'comparable_formats'] @@ -76,40 +69,6 @@ def compare_float(expected, actual, relTol=None, absTol=None): return msg or None -def get_cache_dir(): - cachedir = _get_cachedir() - if cachedir is None: - raise RuntimeError('Could not find a suitable configuration directory') - cache_dir = os.path.join(cachedir, 'test_cache') - if not os.path.exists(cache_dir): - try: - cbook.mkdirs(cache_dir) - except IOError: - return None - if not os.access(cache_dir, os.W_OK): - return None - return cache_dir - - -def get_file_hash(path, block_size=2 ** 20): - md5 = hashlib.md5() - with open(path, 'rb') as fd: - while True: - data = fd.read(block_size) - if not data: - break - md5.update(data) - - if path.endswith('.pdf'): - from matplotlib import checkdep_ghostscript - md5.update(checkdep_ghostscript()[1].encode('utf-8')) - elif path.endswith('.svg'): - from matplotlib import checkdep_inkscape - md5.update(checkdep_inkscape().encode('utf-8')) - - return md5.hexdigest() - - def make_external_conversion_command(cmd): def convert(old, new): cmdline = cmd(old, new) @@ -160,16 +119,20 @@ def comparable_formats(): return ['png'] + list(converter) -def convert(filename, cache): +def convert(filename, cache=None): """ Convert the named file into a png file. Returns the name of the created file. - If *cache* is True, the result of the conversion is cached in - `matplotlib._get_cachedir() + '/test_cache/'`. The caching is based - on a hash of the exact contents of the input file. The is no limit - on the size of the cache, so it may need to be manually cleared - periodically. + Parameters + ---------- + filename : str + cache : ConversionCache, optional + + Returns + ------- + str + The converted file. """ base, extension = filename.rsplit('.', 1) @@ -190,23 +153,12 @@ def convert(filename, cache): # is out of date. if (not os.path.exists(newname) or os.stat(newname).st_mtime < os.stat(filename).st_mtime): - if cache: - cache_dir = get_cache_dir() - else: - cache_dir = None - - if cache_dir is not None: - hash_value = get_file_hash(filename) - new_ext = os.path.splitext(newname)[1] - cached_file = os.path.join(cache_dir, hash_value + new_ext) - if os.path.exists(cached_file): - shutil.copyfile(cached_file, newname) - return newname - + in_cache = cache and cache.get(filename, newname) + if in_cache: + return newname converter[extension](filename, newname) - - if cache_dir is not None: - shutil.copyfile(newname, cached_file) + if cache: + cache.put(filename, newname) return newname @@ -269,7 +221,7 @@ def calculate_rms(expectedImage, actualImage): return rms -def compare_images(expected, actual, tol, in_decorator=False): +def compare_images(expected, actual, tol, in_decorator=False, cache=None): """ Compare two "image" files checking differences within a tolerance. @@ -290,6 +242,7 @@ def compare_images(expected, actual, tol, in_decorator=False): in_decorator : bool If called from image_comparison decorator, this should be True. (default=False) + cache : cache.ConversionCache, optional Example ------- @@ -311,8 +264,8 @@ def compare_images(expected, actual, tol, in_decorator=False): raise IOError('Baseline image %r does not exist.' % expected) if extension != 'png': - actual = convert(actual, False) - expected = convert(expected, True) + actual = convert(actual, cache) + expected = convert(expected, cache) # open the image files and remove the alpha channel (if it exists) expectedImage = _png.read_png_int(expected) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 918bafc873b9..e7bd4d5ed650 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -6,16 +6,44 @@ import matplotlib +def pytest_addoption(parser): + group = parser.getgroup("matplotlib", "matplotlib custom options") + group.addoption("--conversion-cache-max-size", action="store", + help="conversion cache maximum size in bytes") + group.addoption("--conversion-cache-report-misses", + action="store_true", + help="report conversion cache misses") + + def pytest_configure(config): matplotlib.use('agg') matplotlib._called_from_pytest = True matplotlib._init_tests() + max_size = config.getoption('--conversion-cache-max-size') + if max_size is not None: + ccache.conversion_cache = \ + ccache.ConversionCache(max_size=int(max_size)) + else: + ccache.conversion_cache = ccache.ConversionCache() + def pytest_unconfigure(config): + ccache.conversion_cache.expire() matplotlib._called_from_pytest = False +def pytest_terminal_summary(terminalreporter): + tr = terminalreporter + data = ccache.conversion_cache.report() + tr.write_sep('-', 'Image conversion cache report') + tr.write_line('Hit rate: %d/%d' % (len(data['hits']), len(data['gets']))) + if tr.config.getoption('--conversion-cache-report-misses'): + tr.write_line('Missed files:') + for filename in sorted(data['gets'].difference(data['hits'])): + tr.write_line(' %s' % filename) + + @pytest.fixture(autouse=True) def mpl_test_settings(request): from matplotlib.testing.decorators import _do_cleanup diff --git a/lib/matplotlib/testing/conversion_cache.py b/lib/matplotlib/testing/conversion_cache.py new file mode 100644 index 000000000000..876459b54878 --- /dev/null +++ b/lib/matplotlib/testing/conversion_cache.py @@ -0,0 +1,196 @@ +""" +A cache of png files keyed by the MD5 hashes of corresponding svg and +pdf files, to reduce test suite running times for svg and pdf files +that stay exactly the same from one run to the next. + +There is a corresponding nose plugin in testing/nose/plugins and +similar pytest code in conftest.py. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import hashlib +import shutil +import os +import warnings + +from matplotlib import _get_cachedir +from matplotlib import cbook +from matplotlib import checkdep_ghostscript +from matplotlib import checkdep_inkscape + + +class ConversionCache(object): + """A cache that stores png files converted from svg or pdf formats. + + The image comparison test cases compare svg and pdf files by + converting them to png files. When a test case has produced a + file, e.g. result.pdf, it queries this cache by the pathname + '/path/to/result_images/result.pdf'. The cache computes a hash of + the file (and the version of the external software used to convert + the file) and if a result by that hash value is available, writes + the data to the output location supplied by the caller. Otherwise + the test case has to run the conversion and can then insert the + result into the cache. + + Parameters + ---------- + directory : str, optional + Files are stored in this directory, defaults to `'test_cache'` in + the overall Matplotlib cache directory. + max_size : int, optional + The flush method will delete files until their combined size is + under this limit, in bytes. Defaults to 100 megabytes. + + """ + + def __init__(self, directory=None, max_size=int(1e8)): + self.gets = set() + self.hits = set() + if directory is not None: + self.cachedir = directory + else: + self.cachedir = self.get_cache_dir() + self.ensure_cache_dir() + if not isinstance(max_size, int): + raise ValueError("max_size is %s, expected int" % type(max_size)) + self.max_size = max_size + self.cached_ext = '.png' + self.converter_version = {} + try: + self.converter_version['.pdf'] = \ + checkdep_ghostscript()[1].encode('utf-8') + except: + pass + try: + self.converter_version['.svg'] = \ + checkdep_inkscape().encode('utf-8') + except: + pass + self.hash_cache = {} + + def get(self, filename, newname): + """Query the cache. + + Parameters + ---------- + filename : str + Full path to the original file. + newname : str + Path to which the result should be written. + + Returns + ------- + bool + True if the file was found in the cache and is now written + to `newname`. + """ + self.gets.add(filename) + hash_value = self._get_file_hash(filename) + cached_file = os.path.join(self.cachedir, hash_value + self.cached_ext) + if os.path.exists(cached_file): + shutil.copyfile(cached_file, newname) + self.hits.add(filename) + return True + else: + return False + + def put(self, original, converted): + """Insert a file into the cache. + + Parameters + ---------- + original : str + Full path to the original file. + converted : str + Full path to the png file converted from the original. + """ + hash_value = self._get_file_hash(original) + cached_file = os.path.join(self.cachedir, hash_value + self.cached_ext) + shutil.copyfile(converted, cached_file) + + def _get_file_hash(self, path, block_size=2 ** 20): + if path in self.hash_cache: + return self.hash_cache[path] + md5 = hashlib.md5() + with open(path, 'rb') as fd: + while True: + data = fd.read(block_size) + if not data: + break + md5.update(data) + _, ext = os.path.splitext(path) + version_tag = self.converter_version.get(ext) + if version_tag: + md5.update(version_tag) + else: + warnings.warn(("Don't know the external converter for %s, cannot " + "ensure cache invalidation on version update.") + % path) + + result = md5.hexdigest() + self.hash_cache[path] = result + return result + + def report(self): + """Return information about the cache. + + Returns + ------- + r : dict + `r['gets']` is the set of files queried, + `r['hits']` is the set of files found in the cache + """ + return dict(hits=self.hits, gets=self.gets) + + def expire(self): + """Delete cached files until the disk usage is under the limit. + + Orders files by access time, so the least recently used files + get deleted first. + """ + stats = {filename: os.stat(os.path.join(self.cachedir, filename)) + for filename in os.listdir(self.cachedir)} + usage = sum(f.st_size for f in stats.values()) + to_free = usage - self.max_size + if to_free <= 0: + return + + files = sorted(os.listdir(self.cachedir), + key=lambda f: stats[f].st_atime, + reverse=True) + while to_free > 0: + filename = files.pop() + os.remove(os.path.join(self.cachedir, filename)) + to_free -= stats[filename].st_size + + @staticmethod + def get_cache_dir(): + cachedir = _get_cachedir() + if cachedir is None: + raise CacheError('No suitable configuration directory') + cachedir = os.path.join(cachedir, 'test_cache') + return cachedir + + def ensure_cache_dir(self): + if not os.path.exists(self.cachedir): + try: + cbook.mkdirs(self.cachedir) + except IOError as e: + raise CacheError("Error creating cache directory %s: %s" + % (self.cachedir, str(e))) + if not os.access(self.cachedir, os.W_OK): + raise CacheError("Cache directory %s not writable" % self.cachedir) + + +class CacheError(Exception): + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message + + +# A global cache instance, set by the appropriate test runner. +conversion_cache = None diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index a026eb8c02ea..32cd9e9ec21b 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -28,6 +28,7 @@ make_test_filename from . import _copy_metadata, is_called_from_pytest from .exceptions import ImageComparisonFailure +from .conversion_cache import conversion_cache def _knownfailureif(fail_condition, msg=None, known_exception_class=None): @@ -197,7 +198,8 @@ def remove_ticks_and_titles(figure): def _raise_on_image_difference(expected, actual, tol): __tracebackhide__ = True - err = compare_images(expected, actual, tol, in_decorator=True) + err = compare_images(expected, actual, tol, in_decorator=True, + cache=conversion_cache) if not os.path.exists(expected): raise ImageComparisonFailure('image does not exist: %s' % expected) diff --git a/lib/matplotlib/testing/nose/plugins/conversion_cache.py b/lib/matplotlib/testing/nose/plugins/conversion_cache.py new file mode 100644 index 000000000000..720bfcd54f61 --- /dev/null +++ b/lib/matplotlib/testing/nose/plugins/conversion_cache.py @@ -0,0 +1,44 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from pprint import pprint +from nose.plugins import Plugin +import os + +from ... import conversion_cache as ccache + + +class ConversionCache(Plugin): + enabled = True + name = 'conversion-cache' + + def options(self, parser, env=os.environ): + super(ConversionCache, self).configure(parser, env) + parser.add_option("--conversion-cache-max-size", action="store", + dest="conversion_cache_max_size", + help="conversion cache maximum size in bytes") + parser.add_option("--conversion-cache-report-misses", + action="store_true", + dest="conversion_cache_report_misses", + help="report conversion cache misses") + + def configure(self, options, conf): + super(ConversionCache, self).configure(options, conf) + if self.enabled: + max_size = options.conversion_cache_max_size + self.report_misses = options.conversion_cache_report_misses + if max_size is not None: + ccache.conversion_cache = ccache.ConversionCache( + max_size=int(max_size)) + else: + ccache.conversion_cache = ccache.ConversionCache() + + def report(self, stream): + ccache.conversion_cache.expire() + data = ccache.conversion_cache.report() + print("Image conversion cache hit rate: %d/%d" % + (len(data['hits']), len(data['gets'])), file=stream) + if self.report_misses: + print("Missed files:", file=stream) + for filename in sorted(data['gets'].difference(data['hits'])): + print(" %s" % filename, file=stream) diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py new file mode 100644 index 000000000000..b8b05b62378a --- /dev/null +++ b/lib/matplotlib/tests/test_cache.py @@ -0,0 +1,125 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +try: + from unittest import mock +except ImportError: + import mock + +import os +import shutil +import stat +import tempfile + +from nose.tools import raises + +from matplotlib import cbook +from matplotlib.testing.conversion_cache import ConversionCache, CacheError + + +def test_cache_basic(): + tmpdir = tempfile.mkdtemp() + + def intmp(f): + return os.path.join(tmpdir, f) + try: + cache = ConversionCache(intmp('cache')) + with open(intmp('fake.pdf'), 'w') as pdf: + pdf.write('this is a fake pdf file') + with open(intmp('fake.svg'), 'w') as svg: + svg.write('this pretends to be an svg file') + + assert not cache.get(intmp('fake.pdf'), intmp('fakepdf.png')) + assert not cache.get(intmp('fake.svg'), intmp('fakesvg.png')) + assert cache.report() == \ + {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, + 'hits': set()} + + with open(intmp('fakepdf.png'), 'w') as png: + png.write('generated from the pdf file') + cache.put(intmp('fake.pdf'), intmp('fakepdf.png')) + assert cache.get(intmp('fake.pdf'), intmp('copypdf.png')) + with open(intmp('copypdf.png'), 'r') as copy: + assert copy.read() == 'generated from the pdf file' + assert cache.report() == \ + {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, + 'hits': set([intmp('fake.pdf')])} + + with open(intmp('fakesvg.png'), 'w') as png: + png.write('generated from the svg file') + cache.put(intmp('fake.svg'), intmp('fakesvg.png')) + assert cache.get(intmp('fake.svg'), intmp('copysvg.png')) + with open(intmp('copysvg.png'), 'r') as copy: + assert copy.read() == 'generated from the svg file' + assert cache.report() == \ + {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, + 'hits': {intmp('fake.pdf'), intmp('fake.svg')}} + finally: + shutil.rmtree(tmpdir) + + +def test_cache_expire(): + tmpdir = tempfile.mkdtemp() + + def intmp(*f): + return os.path.join(tmpdir, *f) + try: + cache = ConversionCache(intmp('cache'), 10) + for i in range(5): + filename = intmp('cache', 'file%d.png' % i) + with open(filename, 'w') as f: + f.write('1234') + os.utime(filename, (i*1000, i*1000)) + + cache.expire() + assert not os.path.exists(intmp('cache', 'file0.png')) + assert not os.path.exists(intmp('cache', 'file1.png')) + assert not os.path.exists(intmp('cache', 'file2.png')) + assert os.path.exists(intmp('cache', 'file3.png')) + assert os.path.exists(intmp('cache', 'file4.png')) + + with open(intmp('cache', 'onemore.png'), 'w') as f: + f.write('x' * 11) + os.utime(intmp('cache', 'onemore.png'), (5000, 5000)) + + cache.expire() + assert not os.path.exists(intmp('cache', 'file0.png')) + assert not os.path.exists(intmp('cache', 'file1.png')) + assert not os.path.exists(intmp('cache', 'file2.png')) + assert not os.path.exists(intmp('cache', 'file3.png')) + assert not os.path.exists(intmp('cache', 'file4.png')) + assert not os.path.exists(intmp('cache', 'onemore.png')) + + finally: + shutil.rmtree(tmpdir) + + +def test_cache_default_dir(): + try: + path = ConversionCache.get_cache_dir() + assert path.endswith('test_cache') + except CacheError: + pass + + +@raises(CacheError) +@mock.patch('matplotlib.testing.conversion_cache.cbook.mkdirs', + side_effect=IOError) +def test_cache_mkdir_error(mkdirs): + tmpdir = tempfile.mkdtemp() + try: + c = ConversionCache(os.path.join(tmpdir, 'cache')) + finally: + shutil.rmtree(tmpdir) + + +@raises(CacheError) +@mock.patch('matplotlib.testing.conversion_cache.os.access', + side_effect=[False]) +def test_cache_unwritable_error(access): + tmpdir = tempfile.mkdtemp() + cachedir = os.path.join(tmpdir, 'test_cache') + try: + cbook.mkdirs(cachedir) + c = ConversionCache(cachedir) + finally: + shutil.rmtree(tmpdir) From 650ddf28570ebdbcd2337efa1d319f8ed6bed602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jan 2017 18:32:19 +0200 Subject: [PATCH 02/19] Import cbook again Otherwise this breaks after merging to master --- lib/matplotlib/testing/compare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index b2d77e7907e2..941f2f426d1b 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -10,6 +10,7 @@ import numpy as np import matplotlib +from matplotlib import cbook from matplotlib.compat import subprocess from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png From 3b5f636d0b6f9acaac7fb84fd7e3c647beaae4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 21 Jan 2017 19:33:45 +0200 Subject: [PATCH 03/19] Lock the cache directory In case multiple processes use it at the same time. Don't delete the lock file in expire(). --- lib/matplotlib/testing/conversion_cache.py | 46 ++++++++++++---------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/testing/conversion_cache.py b/lib/matplotlib/testing/conversion_cache.py index 876459b54878..0dce47c1f9ec 100644 --- a/lib/matplotlib/testing/conversion_cache.py +++ b/lib/matplotlib/testing/conversion_cache.py @@ -89,12 +89,13 @@ def get(self, filename, newname): self.gets.add(filename) hash_value = self._get_file_hash(filename) cached_file = os.path.join(self.cachedir, hash_value + self.cached_ext) - if os.path.exists(cached_file): - shutil.copyfile(cached_file, newname) - self.hits.add(filename) - return True - else: - return False + with cbook.Locked(self.cachedir): + if os.path.exists(cached_file): + shutil.copyfile(cached_file, newname) + self.hits.add(filename) + return True + else: + return False def put(self, original, converted): """Insert a file into the cache. @@ -108,7 +109,8 @@ def put(self, original, converted): """ hash_value = self._get_file_hash(original) cached_file = os.path.join(self.cachedir, hash_value + self.cached_ext) - shutil.copyfile(converted, cached_file) + with cbook.Locked(self.cachedir): + shutil.copyfile(converted, cached_file) def _get_file_hash(self, path, block_size=2 ** 20): if path in self.hash_cache: @@ -150,20 +152,22 @@ def expire(self): Orders files by access time, so the least recently used files get deleted first. """ - stats = {filename: os.stat(os.path.join(self.cachedir, filename)) - for filename in os.listdir(self.cachedir)} - usage = sum(f.st_size for f in stats.values()) - to_free = usage - self.max_size - if to_free <= 0: - return - - files = sorted(os.listdir(self.cachedir), - key=lambda f: stats[f].st_atime, - reverse=True) - while to_free > 0: - filename = files.pop() - os.remove(os.path.join(self.cachedir, filename)) - to_free -= stats[filename].st_size + with cbook.Locked(self.cachedir): + stats = {filename: os.stat(os.path.join(self.cachedir, filename)) + for filename in os.listdir(self.cachedir) + if filename.endswith(self.cached_ext)} + usage = sum(f.st_size for f in stats.values()) + to_free = usage - self.max_size + if to_free <= 0: + return + + files = sorted(stats.keys(), + key=lambda f: stats[f].st_atime, + reverse=True) + while to_free > 0: + filename = files.pop() + os.remove(os.path.join(self.cachedir, filename)) + to_free -= stats[filename].st_size @staticmethod def get_cache_dir(): From 556fd82eeac1ed8bbc4d6b8bd4e155357124b510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sun, 22 Jan 2017 11:08:27 +0200 Subject: [PATCH 04/19] Communicate node-specific cache reports to master if using xdist See: https://github.com/pytest-dev/pytest-xdist/blob/master/xdist/newhooks.py http://pytest.org/latest/writing_plugins.html#optionally-using-hooks-from-3rd-party-plugins https://github.com/pytest-dev/pytest-xdist/blob/e5d80645347fc7e67efdceb97df8e95d140b283d/testing/acceptance_test.py#L221 --- lib/matplotlib/testing/conftest.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index e7bd4d5ed650..f7e029c12a1d 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -1,6 +1,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from six.moves import reduce import pytest import matplotlib @@ -26,6 +27,8 @@ def pytest_configure(config): ccache.ConversionCache(max_size=int(max_size)) else: ccache.conversion_cache = ccache.ConversionCache() + if config.pluginmanager.hasplugin('xdist'): + config.pluginmanager.register(DeferPlugin()) def pytest_unconfigure(config): @@ -33,10 +36,22 @@ def pytest_unconfigure(config): matplotlib._called_from_pytest = False +def pytest_sessionfinish(session): + if hasattr(session.config, 'slaveoutput'): + session.config.slaveoutput['cache-report'] = ccache.conversion_cache.report() + + def pytest_terminal_summary(terminalreporter): tr = terminalreporter - data = ccache.conversion_cache.report() - tr.write_sep('-', 'Image conversion cache report') + if hasattr(tr.config, 'cache_reports'): + reports = tr.config.cache_reports + data = {'hits': reduce(lambda x, y: x.union(y), + (rep['hits'] for rep in reports)), + 'gets': reduce(lambda x, y: x.union(y), + (rep['gets'] for rep in reports))} + else: + data = ccache.conversion_cache.report() + tr.write_sep('=', 'Image conversion cache report') tr.write_line('Hit rate: %d/%d' % (len(data['hits']), len(data['gets']))) if tr.config.getoption('--conversion-cache-report-misses'): tr.write_line('Missed files:') @@ -81,3 +96,10 @@ def mpl_test_settings(request): plt.switch_backend(prev_backend) _do_cleanup(original_units_registry, original_settings) + + +class DeferPlugin(object): + def pytest_testnodedown(self, node, error): + if not hasattr(node.config, 'cache_reports'): + node.config.cache_reports = [] + node.config.cache_reports.append(node.slaveoutput['cache-report']) From 9f5a06dd2f220de4fc9c3da792680a5846a66e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 21:50:34 +0200 Subject: [PATCH 05/19] Delete the nose plugin --- lib/matplotlib/testing/_nose/__init__.py | 3 +- .../testing/nose/plugins/conversion_cache.py | 44 ------------------- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 lib/matplotlib/testing/nose/plugins/conversion_cache.py diff --git a/lib/matplotlib/testing/_nose/__init__.py b/lib/matplotlib/testing/_nose/__init__.py index 2f64f440dc7e..d513c7b14f4b 100644 --- a/lib/matplotlib/testing/_nose/__init__.py +++ b/lib/matplotlib/testing/_nose/__init__.py @@ -7,10 +7,9 @@ def get_extra_test_plugins(): from .plugins.performgc import PerformGC from .plugins.knownfailure import KnownFailure - from .plugins.conversion_cache import ConversionCache from nose.plugins import attrib - return [PerformGC, KnownFailure, attrib.Plugin, ConversionCache] + return [PerformGC, KnownFailure, attrib.Plugin] def get_env(): diff --git a/lib/matplotlib/testing/nose/plugins/conversion_cache.py b/lib/matplotlib/testing/nose/plugins/conversion_cache.py deleted file mode 100644 index 720bfcd54f61..000000000000 --- a/lib/matplotlib/testing/nose/plugins/conversion_cache.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import (absolute_import, division, print_function, - unicode_literals) - -from pprint import pprint -from nose.plugins import Plugin -import os - -from ... import conversion_cache as ccache - - -class ConversionCache(Plugin): - enabled = True - name = 'conversion-cache' - - def options(self, parser, env=os.environ): - super(ConversionCache, self).configure(parser, env) - parser.add_option("--conversion-cache-max-size", action="store", - dest="conversion_cache_max_size", - help="conversion cache maximum size in bytes") - parser.add_option("--conversion-cache-report-misses", - action="store_true", - dest="conversion_cache_report_misses", - help="report conversion cache misses") - - def configure(self, options, conf): - super(ConversionCache, self).configure(options, conf) - if self.enabled: - max_size = options.conversion_cache_max_size - self.report_misses = options.conversion_cache_report_misses - if max_size is not None: - ccache.conversion_cache = ccache.ConversionCache( - max_size=int(max_size)) - else: - ccache.conversion_cache = ccache.ConversionCache() - - def report(self, stream): - ccache.conversion_cache.expire() - data = ccache.conversion_cache.report() - print("Image conversion cache hit rate: %d/%d" % - (len(data['hits']), len(data['gets'])), file=stream) - if self.report_misses: - print("Missed files:", file=stream) - for filename in sorted(data['gets'].difference(data['hits'])): - print(" %s" % filename, file=stream) From 33f2528cf9e68b5adcf5f6a94de54f5e07450c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 22:05:43 +0200 Subject: [PATCH 06/19] Reinstate deleted functions With a deprecation decorator --- lib/matplotlib/testing/compare.py | 20 +++++++++++++++ lib/matplotlib/testing/conversion_cache.py | 29 ++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 941f2f426d1b..105f303e30a0 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -12,6 +12,7 @@ import matplotlib from matplotlib import cbook from matplotlib.compat import subprocess +from matplotlib.testing import conversion_cache as ccache from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png @@ -120,6 +121,25 @@ def comparable_formats(): return ['png'] + list(converter) +@cbook.deprecated('2.1', addendum='Use ConversionCache instead') +def get_cache_dir(): + return ccache.ConversionCache.get_cache_dir() + + +@cbook.deprecated('2.1', addendum='Use ConversionCache instead') +def get_file_hash(path, block_size=2 ** 20): + if path.endswith('.pdf'): + from matplotlib import checkdep_ghostscript + version_tag = checkdep_ghostscript()[1].encode('utf-8') + elif path.endswith('.svg'): + from matplotlib import checkdep_inkscape + version_tag = checkdep_inkscape().encode('utf-8') + else: + version_tag = None + return ccache.ConversionCache._get_file_hash_static( + path, block_size, version_tag) + + def convert(filename, cache=None): """ Convert the named file into a png file. Returns the name of the diff --git a/lib/matplotlib/testing/conversion_cache.py b/lib/matplotlib/testing/conversion_cache.py index 0dce47c1f9ec..4fa8f6217973 100644 --- a/lib/matplotlib/testing/conversion_cache.py +++ b/lib/matplotlib/testing/conversion_cache.py @@ -115,6 +115,22 @@ def put(self, original, converted): def _get_file_hash(self, path, block_size=2 ** 20): if path in self.hash_cache: return self.hash_cache[path] + _, ext = os.path.splitext(path) + version_tag = self.converter_version.get(ext) + if version_tag is None: + warnings.warn( + ("Don't know the external converter for files with extension " + "%s, cannot ensure cache invalidation on version update.") + % ext) + result = self._get_file_hash_static(path, block_size, version_tag) + self.hash_cache[path] = result + return result + + @staticmethod + def _get_file_hash_static(path, block_size, version_tag): + # the parts of _get_file_hash that are called from the deprecated + # compare.get_file_hash; can merge into _get_file_hash once that + # function is removed md5 = hashlib.md5() with open(path, 'rb') as fd: while True: @@ -122,18 +138,9 @@ def _get_file_hash(self, path, block_size=2 ** 20): if not data: break md5.update(data) - _, ext = os.path.splitext(path) - version_tag = self.converter_version.get(ext) - if version_tag: + if version_tag is not None: md5.update(version_tag) - else: - warnings.warn(("Don't know the external converter for %s, cannot " - "ensure cache invalidation on version update.") - % path) - - result = md5.hexdigest() - self.hash_cache[path] = result - return result + return md5.hexdigest() def report(self): """Return information about the cache. From 2136a264d2eb49a04c9b42940b3011e52af55f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 22:13:19 +0200 Subject: [PATCH 07/19] Make conversion_cache private --- .../testing/{conversion_cache.py => _conversion_cache.py} | 0 lib/matplotlib/testing/compare.py | 2 +- lib/matplotlib/testing/conftest.py | 2 +- lib/matplotlib/testing/decorators.py | 4 ++-- lib/matplotlib/tests/test_cache.py | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) rename lib/matplotlib/testing/{conversion_cache.py => _conversion_cache.py} (100%) diff --git a/lib/matplotlib/testing/conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py similarity index 100% rename from lib/matplotlib/testing/conversion_cache.py rename to lib/matplotlib/testing/_conversion_cache.py diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 105f303e30a0..6df1b3767ac8 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -12,7 +12,7 @@ import matplotlib from matplotlib import cbook from matplotlib.compat import subprocess -from matplotlib.testing import conversion_cache as ccache +from matplotlib.testing import _conversion_cache as ccache from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib import _png diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index f7e029c12a1d..889b78f3dd66 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -1,10 +1,10 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) - from six.moves import reduce import pytest import matplotlib +from matplotlib.testing import _conversion_cache as ccache def pytest_addoption(parser): diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 32cd9e9ec21b..f6635a3dca84 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -28,7 +28,7 @@ make_test_filename from . import _copy_metadata, is_called_from_pytest from .exceptions import ImageComparisonFailure -from .conversion_cache import conversion_cache +from ._conversion_cache import _conversion_cache def _knownfailureif(fail_condition, msg=None, known_exception_class=None): @@ -199,7 +199,7 @@ def _raise_on_image_difference(expected, actual, tol): __tracebackhide__ = True err = compare_images(expected, actual, tol, in_decorator=True, - cache=conversion_cache) + cache=_conversion_cache) if not os.path.exists(expected): raise ImageComparisonFailure('image does not exist: %s' % expected) diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index b8b05b62378a..fa520b2f07be 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -101,8 +101,8 @@ def test_cache_default_dir(): pass -@raises(CacheError) -@mock.patch('matplotlib.testing.conversion_cache.cbook.mkdirs', +@raises(_CacheError) +@mock.patch('matplotlib.testing._conversion_cache.cbook.mkdirs', side_effect=IOError) def test_cache_mkdir_error(mkdirs): tmpdir = tempfile.mkdtemp() @@ -112,8 +112,8 @@ def test_cache_mkdir_error(mkdirs): shutil.rmtree(tmpdir) -@raises(CacheError) -@mock.patch('matplotlib.testing.conversion_cache.os.access', +@raises(_CacheError) +@mock.patch('matplotlib.testing._conversion_cache.os.access', side_effect=[False]) def test_cache_unwritable_error(access): tmpdir = tempfile.mkdtemp() From 6995a27107e4f9ff6cff509d4ac398359ad8dccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 22:17:26 +0200 Subject: [PATCH 08/19] Make globals private --- lib/matplotlib/testing/_conversion_cache.py | 15 ++++++++------- lib/matplotlib/testing/compare.py | 12 ++++++------ lib/matplotlib/testing/conftest.py | 13 +++++++------ lib/matplotlib/tests/test_cache.py | 14 +++++++------- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index 4fa8f6217973..bdd01e144a63 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -21,7 +21,7 @@ from matplotlib import checkdep_inkscape -class ConversionCache(object): +class _ConversionCache(object): """A cache that stores png files converted from svg or pdf formats. The image comparison test cases compare svg and pdf files by @@ -180,7 +180,7 @@ def expire(self): def get_cache_dir(): cachedir = _get_cachedir() if cachedir is None: - raise CacheError('No suitable configuration directory') + raise _CacheError('No suitable configuration directory') cachedir = os.path.join(cachedir, 'test_cache') return cachedir @@ -189,13 +189,14 @@ def ensure_cache_dir(self): try: cbook.mkdirs(self.cachedir) except IOError as e: - raise CacheError("Error creating cache directory %s: %s" - % (self.cachedir, str(e))) + raise _CacheError("Error creating cache directory %s: %s" + % (self.cachedir, str(e))) if not os.access(self.cachedir, os.W_OK): - raise CacheError("Cache directory %s not writable" % self.cachedir) + raise _CacheError("Cache directory %s not writable" + % self.cachedir) -class CacheError(Exception): +class _CacheError(Exception): def __init__(self, message): self.message = message @@ -204,4 +205,4 @@ def __str__(self): # A global cache instance, set by the appropriate test runner. -conversion_cache = None +_conversion_cache = None diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 6df1b3767ac8..e3e6415a6a13 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -121,12 +121,12 @@ def comparable_formats(): return ['png'] + list(converter) -@cbook.deprecated('2.1', addendum='Use ConversionCache instead') +@cbook.deprecated('2.1', addendum='Use _ConversionCache instead') def get_cache_dir(): - return ccache.ConversionCache.get_cache_dir() + return ccache._ConversionCache.get_cache_dir() -@cbook.deprecated('2.1', addendum='Use ConversionCache instead') +@cbook.deprecated('2.1', addendum='Use _ConversionCache instead') def get_file_hash(path, block_size=2 ** 20): if path.endswith('.pdf'): from matplotlib import checkdep_ghostscript @@ -136,7 +136,7 @@ def get_file_hash(path, block_size=2 ** 20): version_tag = checkdep_inkscape().encode('utf-8') else: version_tag = None - return ccache.ConversionCache._get_file_hash_static( + return ccache._ConversionCache._get_file_hash_static( path, block_size, version_tag) @@ -148,7 +148,7 @@ def convert(filename, cache=None): Parameters ---------- filename : str - cache : ConversionCache, optional + cache : _ConversionCache, optional Returns ------- @@ -263,7 +263,7 @@ def compare_images(expected, actual, tol, in_decorator=False, cache=None): in_decorator : bool If called from image_comparison decorator, this should be True. (default=False) - cache : cache.ConversionCache, optional + cache : matplotlib.testing._conversion_cache._ConversionCache, optional Example ------- diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 889b78f3dd66..e01078ffe3f1 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -23,22 +23,23 @@ def pytest_configure(config): max_size = config.getoption('--conversion-cache-max-size') if max_size is not None: - ccache.conversion_cache = \ - ccache.ConversionCache(max_size=int(max_size)) + ccache._conversion_cache = \ + ccache._ConversionCache(max_size=int(max_size)) else: - ccache.conversion_cache = ccache.ConversionCache() + ccache._conversion_cache = ccache._ConversionCache() if config.pluginmanager.hasplugin('xdist'): config.pluginmanager.register(DeferPlugin()) def pytest_unconfigure(config): - ccache.conversion_cache.expire() + ccache._conversion_cache.expire() matplotlib._called_from_pytest = False def pytest_sessionfinish(session): if hasattr(session.config, 'slaveoutput'): - session.config.slaveoutput['cache-report'] = ccache.conversion_cache.report() + session.config.slaveoutput['cache-report'] = \ + ccache._conversion_cache.report() def pytest_terminal_summary(terminalreporter): @@ -50,7 +51,7 @@ def pytest_terminal_summary(terminalreporter): 'gets': reduce(lambda x, y: x.union(y), (rep['gets'] for rep in reports))} else: - data = ccache.conversion_cache.report() + data = ccache._conversion_cache.report() tr.write_sep('=', 'Image conversion cache report') tr.write_line('Hit rate: %d/%d' % (len(data['hits']), len(data['gets']))) if tr.config.getoption('--conversion-cache-report-misses'): diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index fa520b2f07be..3884ab4d3deb 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -13,7 +13,7 @@ from nose.tools import raises from matplotlib import cbook -from matplotlib.testing.conversion_cache import ConversionCache, CacheError +from matplotlib.testing._conversion_cache import _ConversionCache, _CacheError def test_cache_basic(): @@ -22,7 +22,7 @@ def test_cache_basic(): def intmp(f): return os.path.join(tmpdir, f) try: - cache = ConversionCache(intmp('cache')) + cache = _ConversionCache(intmp('cache')) with open(intmp('fake.pdf'), 'w') as pdf: pdf.write('this is a fake pdf file') with open(intmp('fake.svg'), 'w') as svg: @@ -63,7 +63,7 @@ def test_cache_expire(): def intmp(*f): return os.path.join(tmpdir, *f) try: - cache = ConversionCache(intmp('cache'), 10) + cache = _ConversionCache(intmp('cache'), 10) for i in range(5): filename = intmp('cache', 'file%d.png' % i) with open(filename, 'w') as f: @@ -95,9 +95,9 @@ def intmp(*f): def test_cache_default_dir(): try: - path = ConversionCache.get_cache_dir() + path = _ConversionCache.get_cache_dir() assert path.endswith('test_cache') - except CacheError: + except _CacheError: pass @@ -107,7 +107,7 @@ def test_cache_default_dir(): def test_cache_mkdir_error(mkdirs): tmpdir = tempfile.mkdtemp() try: - c = ConversionCache(os.path.join(tmpdir, 'cache')) + c = _ConversionCache(os.path.join(tmpdir, 'cache')) finally: shutil.rmtree(tmpdir) @@ -120,6 +120,6 @@ def test_cache_unwritable_error(access): cachedir = os.path.join(tmpdir, 'test_cache') try: cbook.mkdirs(cachedir) - c = ConversionCache(cachedir) + c = _ConversionCache(cachedir) finally: shutil.rmtree(tmpdir) From 9ea66ddf0f97ce30800c6c7081f2ac7d23e14f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 22:22:26 +0200 Subject: [PATCH 09/19] Improve wording --- lib/matplotlib/testing/_conversion_cache.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index bdd01e144a63..9571365f590c 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -37,7 +37,7 @@ class _ConversionCache(object): Parameters ---------- directory : str, optional - Files are stored in this directory, defaults to `'test_cache'` in + Files are stored in this directory. Defaults to `'test_cache'` in the overall Matplotlib cache directory. max_size : int, optional The flush method will delete files until their combined size is @@ -54,7 +54,8 @@ def __init__(self, directory=None, max_size=int(1e8)): self.cachedir = self.get_cache_dir() self.ensure_cache_dir() if not isinstance(max_size, int): - raise ValueError("max_size is %s, expected int" % type(max_size)) + raise ValueError("max_size is of type %s, expected int" % + type(max_size)) self.max_size = max_size self.cached_ext = '.png' self.converter_version = {} @@ -120,7 +121,9 @@ def _get_file_hash(self, path, block_size=2 ** 20): if version_tag is None: warnings.warn( ("Don't know the external converter for files with extension " - "%s, cannot ensure cache invalidation on version update.") + "%s, cannot ensure cache invalidation on version update. " + "Either the relevant conversion program is missing or caching" + " is attempted for an unknown file type.") % ext) result = self._get_file_hash_static(path, block_size, version_tag) self.hash_cache[path] = result From 3751f01f5ca0a10722a07b4664f39fae79bdb954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 25 Jan 2017 22:26:19 +0200 Subject: [PATCH 10/19] mkdirs throws OSError --- lib/matplotlib/testing/_conversion_cache.py | 2 +- lib/matplotlib/tests/test_cache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index 9571365f590c..c46945da8d0a 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -191,7 +191,7 @@ def ensure_cache_dir(self): if not os.path.exists(self.cachedir): try: cbook.mkdirs(self.cachedir) - except IOError as e: + except OSError as e: raise _CacheError("Error creating cache directory %s: %s" % (self.cachedir, str(e))) if not os.access(self.cachedir, os.W_OK): diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index 3884ab4d3deb..ae3ae3999eac 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -103,7 +103,7 @@ def test_cache_default_dir(): @raises(_CacheError) @mock.patch('matplotlib.testing._conversion_cache.cbook.mkdirs', - side_effect=IOError) + side_effect=OSError) def test_cache_mkdir_error(mkdirs): tmpdir = tempfile.mkdtemp() try: From a70081e982b33485b66057c222365b3c1aa8ef95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Mon, 30 Jan 2017 18:01:01 +0200 Subject: [PATCH 11/19] Improve documentation in response to review No need to point users to private classes --- lib/matplotlib/testing/_conversion_cache.py | 2 ++ lib/matplotlib/testing/compare.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index c46945da8d0a..ab19219b5d12 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -46,7 +46,9 @@ class _ConversionCache(object): """ def __init__(self, directory=None, max_size=int(1e8)): + # gets is the set of filenames queried from the cache self.gets = set() + # hits is the set of filenames found in the cache when queried self.hits = set() if directory is not None: self.cachedir = directory diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index e3e6415a6a13..bf873d57ed54 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -121,12 +121,12 @@ def comparable_formats(): return ['png'] + list(converter) -@cbook.deprecated('2.1', addendum='Use _ConversionCache instead') +@cbook.deprecated('2.1') def get_cache_dir(): return ccache._ConversionCache.get_cache_dir() -@cbook.deprecated('2.1', addendum='Use _ConversionCache instead') +@cbook.deprecated('2.1') def get_file_hash(path, block_size=2 ** 20): if path.endswith('.pdf'): from matplotlib import checkdep_ghostscript From 85f54dec1f5f15c4813ac2a15e3aaa5ee82eb300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Mon, 30 Jan 2017 18:01:53 +0200 Subject: [PATCH 12/19] Use pytest facilities --- lib/matplotlib/tests/test_cache.py | 137 +++++++++++++---------------- 1 file changed, 61 insertions(+), 76 deletions(-) diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index ae3ae3999eac..ce3737369613 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -6,91 +6,78 @@ import mock import os -import shutil -import stat -import tempfile - -from nose.tools import raises from matplotlib import cbook from matplotlib.testing._conversion_cache import _ConversionCache, _CacheError +import pytest -def test_cache_basic(): - tmpdir = tempfile.mkdtemp() - +def test_cache_basic(tmpdir): def intmp(f): - return os.path.join(tmpdir, f) + return tmpdir.join(f) + def fname(f): + return str(intmp(f)) try: - cache = _ConversionCache(intmp('cache')) - with open(intmp('fake.pdf'), 'w') as pdf: - pdf.write('this is a fake pdf file') - with open(intmp('fake.svg'), 'w') as svg: - svg.write('this pretends to be an svg file') - - assert not cache.get(intmp('fake.pdf'), intmp('fakepdf.png')) - assert not cache.get(intmp('fake.svg'), intmp('fakesvg.png')) + cache = _ConversionCache(fname('cache')) + intmp('fake.pdf').write_binary(b'this is a fake pdf file') + intmp('fake.svg').write_binary(b'this pretends to be an svg file') + assert not cache.get(fname('fake.pdf'), fname('fakepdf.png')) + assert not cache.get(fname('fake.svg'), fname('fakesvg.png')) assert cache.report() == \ - {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, + {'gets': {fname('fake.pdf'), fname('fake.svg')}, 'hits': set()} - with open(intmp('fakepdf.png'), 'w') as png: - png.write('generated from the pdf file') - cache.put(intmp('fake.pdf'), intmp('fakepdf.png')) - assert cache.get(intmp('fake.pdf'), intmp('copypdf.png')) - with open(intmp('copypdf.png'), 'r') as copy: - assert copy.read() == 'generated from the pdf file' + intmp('fakepdf.png').write_binary(b'generated from the pdf file') + cache.put(fname('fake.pdf'), fname('fakepdf.png')) + assert cache.get(fname('fake.pdf'), fname('copypdf.png')) + assert intmp('copypdf.png').read() == 'generated from the pdf file' assert cache.report() == \ - {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, - 'hits': set([intmp('fake.pdf')])} - - with open(intmp('fakesvg.png'), 'w') as png: - png.write('generated from the svg file') - cache.put(intmp('fake.svg'), intmp('fakesvg.png')) - assert cache.get(intmp('fake.svg'), intmp('copysvg.png')) - with open(intmp('copysvg.png'), 'r') as copy: - assert copy.read() == 'generated from the svg file' + {'gets': {fname('fake.pdf'), fname('fake.svg')}, + 'hits': {fname('fake.pdf')}} + + intmp('fakesvg.png').write_binary(b'generated from the svg file') + cache.put(fname('fake.svg'), fname('fakesvg.png')) + assert cache.get(fname('fake.svg'), fname('copysvg.png')) + assert intmp('copysvg.png').read() == 'generated from the svg file' assert cache.report() == \ - {'gets': {intmp('fake.pdf'), intmp('fake.svg')}, - 'hits': {intmp('fake.pdf'), intmp('fake.svg')}} + {'gets': {fname('fake.pdf'), fname('fake.svg')}, + 'hits': {fname('fake.pdf'), fname('fake.svg')}} finally: - shutil.rmtree(tmpdir) - + tmpdir.remove(rec=1) -def test_cache_expire(): - tmpdir = tempfile.mkdtemp() +def test_cache_expire(tmpdir): def intmp(*f): - return os.path.join(tmpdir, *f) + return tmpdir.join(*f) + def fname(*f): + return str(intmp(*f)) try: - cache = _ConversionCache(intmp('cache'), 10) + cache = _ConversionCache(fname('cache'), 10) for i in range(5): - filename = intmp('cache', 'file%d.png' % i) - with open(filename, 'w') as f: - f.write('1234') - os.utime(filename, (i*1000, i*1000)) + pngfile = intmp('cache', 'file%d.png' % i) + pngfile.write_binary(b'1234') + os.utime(str(pngfile), (i*1000, i*1000)) cache.expire() - assert not os.path.exists(intmp('cache', 'file0.png')) - assert not os.path.exists(intmp('cache', 'file1.png')) - assert not os.path.exists(intmp('cache', 'file2.png')) - assert os.path.exists(intmp('cache', 'file3.png')) - assert os.path.exists(intmp('cache', 'file4.png')) + assert not os.path.exists(fname('cache', 'file0.png')) + assert not os.path.exists(fname('cache', 'file1.png')) + assert not os.path.exists(fname('cache', 'file2.png')) + assert os.path.exists(fname('cache', 'file3.png')) + assert os.path.exists(fname('cache', 'file4.png')) - with open(intmp('cache', 'onemore.png'), 'w') as f: - f.write('x' * 11) - os.utime(intmp('cache', 'onemore.png'), (5000, 5000)) + intmp('cache', 'onemore.png').write_binary(b'x' * 11) + os.utime(fname('cache', 'onemore.png'), (5000, 5000)) cache.expire() - assert not os.path.exists(intmp('cache', 'file0.png')) - assert not os.path.exists(intmp('cache', 'file1.png')) - assert not os.path.exists(intmp('cache', 'file2.png')) - assert not os.path.exists(intmp('cache', 'file3.png')) - assert not os.path.exists(intmp('cache', 'file4.png')) - assert not os.path.exists(intmp('cache', 'onemore.png')) + assert not os.path.exists(fname('cache', 'file0.png')) + assert not os.path.exists(fname('cache', 'file1.png')) + assert not os.path.exists(fname('cache', 'file2.png')) + assert not os.path.exists(fname('cache', 'file3.png')) + assert not os.path.exists(fname('cache', 'file4.png')) + assert not os.path.exists(fname('cache', 'onemore.png')) finally: - shutil.rmtree(tmpdir) + tmpdir.remove(rec=1) def test_cache_default_dir(): @@ -101,25 +88,23 @@ def test_cache_default_dir(): pass -@raises(_CacheError) @mock.patch('matplotlib.testing._conversion_cache.cbook.mkdirs', side_effect=OSError) -def test_cache_mkdir_error(mkdirs): - tmpdir = tempfile.mkdtemp() - try: - c = _ConversionCache(os.path.join(tmpdir, 'cache')) - finally: - shutil.rmtree(tmpdir) +def test_cache_mkdir_error(mkdirs, tmpdir): + with pytest.raises(_CacheError): + try: + c = _ConversionCache(str(tmpdir.join('cache'))) + finally: + tmpdir.remove(rec=1) -@raises(_CacheError) @mock.patch('matplotlib.testing._conversion_cache.os.access', side_effect=[False]) -def test_cache_unwritable_error(access): - tmpdir = tempfile.mkdtemp() - cachedir = os.path.join(tmpdir, 'test_cache') - try: - cbook.mkdirs(cachedir) - c = _ConversionCache(cachedir) - finally: - shutil.rmtree(tmpdir) +def test_cache_unwritable_error(access, tmpdir): + with pytest.raises(_CacheError): + cachedir = tmpdir.join('cache') + cachedir.ensure(dir=True) + try: + c = _ConversionCache(str(cachedir)) + finally: + tmpdir.remove(rec=1) From 0d51e03b702959ca9235eb200e76c08b18f685dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Mon, 30 Jan 2017 18:08:52 +0200 Subject: [PATCH 13/19] Simplify gs and inkscape version checking logic --- lib/matplotlib/testing/_conversion_cache.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index ab19219b5d12..536e86d2093b 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -61,16 +61,12 @@ def __init__(self, directory=None, max_size=int(1e8)): self.max_size = max_size self.cached_ext = '.png' self.converter_version = {} - try: - self.converter_version['.pdf'] = \ - checkdep_ghostscript()[1].encode('utf-8') - except: - pass - try: - self.converter_version['.svg'] = \ - checkdep_inkscape().encode('utf-8') - except: - pass + _, gs_version = checkdep_ghostscript() + if gs_version is not None: + self.converter_version['.pdf'] = gs_version.encode('utf-8') + is_version = checkdep_inkscape() + if is_version is not None: + self.converter_version['.svg'] = is_version.encode('utf-8') self.hash_cache = {} def get(self, filename, newname): From 9de06c0fd03c9b752c93446d7d368eade1d6d459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Mon, 30 Jan 2017 22:18:25 +0200 Subject: [PATCH 14/19] pep8 fixes --- lib/matplotlib/tests/test_cache.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index ce3737369613..2924c1c49741 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -15,8 +15,10 @@ def test_cache_basic(tmpdir): def intmp(f): return tmpdir.join(f) + def fname(f): return str(intmp(f)) + try: cache = _ConversionCache(fname('cache')) intmp('fake.pdf').write_binary(b'this is a fake pdf file') @@ -49,8 +51,10 @@ def fname(f): def test_cache_expire(tmpdir): def intmp(*f): return tmpdir.join(*f) + def fname(*f): return str(intmp(*f)) + try: cache = _ConversionCache(fname('cache'), 10) for i in range(5): From 6007b378a52319d40581e73500ac41375c158bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 18 Feb 2017 18:18:01 +0200 Subject: [PATCH 15/19] There is no nose plugin any more --- lib/matplotlib/testing/_conversion_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/testing/_conversion_cache.py b/lib/matplotlib/testing/_conversion_cache.py index 536e86d2093b..85d2a0923d59 100644 --- a/lib/matplotlib/testing/_conversion_cache.py +++ b/lib/matplotlib/testing/_conversion_cache.py @@ -3,8 +3,8 @@ pdf files, to reduce test suite running times for svg and pdf files that stay exactly the same from one run to the next. -There is a corresponding nose plugin in testing/nose/plugins and -similar pytest code in conftest.py. +The test setup in matplotlib.tests.conftest sets this up for image +comparison tests and allows specifying the maximum cache size. """ from __future__ import (absolute_import, division, print_function, From e52cdd21dc14c0f167e345fdb2103946a691c0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Mon, 20 Feb 2017 14:33:09 +0200 Subject: [PATCH 16/19] Import the pytest hooks in the other conftest.py files This way py.test lib/matplotlib/tests/test_artist.py --conversion-cache-report-misses -n 2 works for me even though the options don't get documented in "py.test --help". --- lib/matplotlib/sphinxext/tests/conftest.py | 5 +++-- lib/matplotlib/tests/conftest.py | 6 ++++-- lib/mpl_toolkits/tests/conftest.py | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/sphinxext/tests/conftest.py b/lib/matplotlib/sphinxext/tests/conftest.py index dcdc3612eecb..c92a63562827 100644 --- a/lib/matplotlib/sphinxext/tests/conftest.py +++ b/lib/matplotlib/sphinxext/tests/conftest.py @@ -1,5 +1,6 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -from matplotlib.testing.conftest import (mpl_test_settings, - pytest_configure, pytest_unconfigure) +from matplotlib.testing.conftest import ( + mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + pytest_sessionfinish, pytest_terminal_summary) diff --git a/lib/matplotlib/tests/conftest.py b/lib/matplotlib/tests/conftest.py index dcdc3612eecb..de366f7bffa1 100644 --- a/lib/matplotlib/tests/conftest.py +++ b/lib/matplotlib/tests/conftest.py @@ -1,5 +1,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -from matplotlib.testing.conftest import (mpl_test_settings, - pytest_configure, pytest_unconfigure) +from matplotlib.testing.conftest import ( + mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + pytest_sessionfinish, pytest_terminal_summary) + diff --git a/lib/mpl_toolkits/tests/conftest.py b/lib/mpl_toolkits/tests/conftest.py index dcdc3612eecb..c92a63562827 100644 --- a/lib/mpl_toolkits/tests/conftest.py +++ b/lib/mpl_toolkits/tests/conftest.py @@ -1,5 +1,6 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -from matplotlib.testing.conftest import (mpl_test_settings, - pytest_configure, pytest_unconfigure) +from matplotlib.testing.conftest import ( + mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + pytest_sessionfinish, pytest_terminal_summary) From e5184bbe8fbfc38beb211ee749fd7d4425f6b40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 22 Feb 2017 10:07:03 +0200 Subject: [PATCH 17/19] Omit unnecessary tmpdir deletions It seems that the tmpdir fixture will take care of them after three test runs. http://doc.pytest.org/en/latest/tmpdir.html#the-default-base-temporary-directory --- lib/matplotlib/tests/test_cache.py | 111 +++++++++++++---------------- 1 file changed, 49 insertions(+), 62 deletions(-) diff --git a/lib/matplotlib/tests/test_cache.py b/lib/matplotlib/tests/test_cache.py index 2924c1c49741..d8b0dee7eeb9 100644 --- a/lib/matplotlib/tests/test_cache.py +++ b/lib/matplotlib/tests/test_cache.py @@ -19,33 +19,30 @@ def intmp(f): def fname(f): return str(intmp(f)) - try: - cache = _ConversionCache(fname('cache')) - intmp('fake.pdf').write_binary(b'this is a fake pdf file') - intmp('fake.svg').write_binary(b'this pretends to be an svg file') - assert not cache.get(fname('fake.pdf'), fname('fakepdf.png')) - assert not cache.get(fname('fake.svg'), fname('fakesvg.png')) - assert cache.report() == \ - {'gets': {fname('fake.pdf'), fname('fake.svg')}, - 'hits': set()} - - intmp('fakepdf.png').write_binary(b'generated from the pdf file') - cache.put(fname('fake.pdf'), fname('fakepdf.png')) - assert cache.get(fname('fake.pdf'), fname('copypdf.png')) - assert intmp('copypdf.png').read() == 'generated from the pdf file' - assert cache.report() == \ - {'gets': {fname('fake.pdf'), fname('fake.svg')}, - 'hits': {fname('fake.pdf')}} - - intmp('fakesvg.png').write_binary(b'generated from the svg file') - cache.put(fname('fake.svg'), fname('fakesvg.png')) - assert cache.get(fname('fake.svg'), fname('copysvg.png')) - assert intmp('copysvg.png').read() == 'generated from the svg file' - assert cache.report() == \ - {'gets': {fname('fake.pdf'), fname('fake.svg')}, - 'hits': {fname('fake.pdf'), fname('fake.svg')}} - finally: - tmpdir.remove(rec=1) + cache = _ConversionCache(fname('cache')) + intmp('fake.pdf').write_binary(b'this is a fake pdf file') + intmp('fake.svg').write_binary(b'this pretends to be an svg file') + assert not cache.get(fname('fake.pdf'), fname('fakepdf.png')) + assert not cache.get(fname('fake.svg'), fname('fakesvg.png')) + assert cache.report() == \ + {'gets': {fname('fake.pdf'), fname('fake.svg')}, + 'hits': set()} + + intmp('fakepdf.png').write_binary(b'generated from the pdf file') + cache.put(fname('fake.pdf'), fname('fakepdf.png')) + assert cache.get(fname('fake.pdf'), fname('copypdf.png')) + assert intmp('copypdf.png').read() == 'generated from the pdf file' + assert cache.report() == \ + {'gets': {fname('fake.pdf'), fname('fake.svg')}, + 'hits': {fname('fake.pdf')}} + + intmp('fakesvg.png').write_binary(b'generated from the svg file') + cache.put(fname('fake.svg'), fname('fakesvg.png')) + assert cache.get(fname('fake.svg'), fname('copysvg.png')) + assert intmp('copysvg.png').read() == 'generated from the svg file' + assert cache.report() == \ + {'gets': {fname('fake.pdf'), fname('fake.svg')}, + 'hits': {fname('fake.pdf'), fname('fake.svg')}} def test_cache_expire(tmpdir): @@ -55,33 +52,29 @@ def intmp(*f): def fname(*f): return str(intmp(*f)) - try: - cache = _ConversionCache(fname('cache'), 10) - for i in range(5): - pngfile = intmp('cache', 'file%d.png' % i) - pngfile.write_binary(b'1234') - os.utime(str(pngfile), (i*1000, i*1000)) - - cache.expire() - assert not os.path.exists(fname('cache', 'file0.png')) - assert not os.path.exists(fname('cache', 'file1.png')) - assert not os.path.exists(fname('cache', 'file2.png')) - assert os.path.exists(fname('cache', 'file3.png')) - assert os.path.exists(fname('cache', 'file4.png')) - - intmp('cache', 'onemore.png').write_binary(b'x' * 11) - os.utime(fname('cache', 'onemore.png'), (5000, 5000)) - - cache.expire() - assert not os.path.exists(fname('cache', 'file0.png')) - assert not os.path.exists(fname('cache', 'file1.png')) - assert not os.path.exists(fname('cache', 'file2.png')) - assert not os.path.exists(fname('cache', 'file3.png')) - assert not os.path.exists(fname('cache', 'file4.png')) - assert not os.path.exists(fname('cache', 'onemore.png')) - - finally: - tmpdir.remove(rec=1) + cache = _ConversionCache(fname('cache'), 10) + for i in range(5): + pngfile = intmp('cache', 'file%d.png' % i) + pngfile.write_binary(b'1234') + os.utime(str(pngfile), (i*1000, i*1000)) + + cache.expire() + assert not os.path.exists(fname('cache', 'file0.png')) + assert not os.path.exists(fname('cache', 'file1.png')) + assert not os.path.exists(fname('cache', 'file2.png')) + assert os.path.exists(fname('cache', 'file3.png')) + assert os.path.exists(fname('cache', 'file4.png')) + + intmp('cache', 'onemore.png').write_binary(b'x' * 11) + os.utime(fname('cache', 'onemore.png'), (5000, 5000)) + + cache.expire() + assert not os.path.exists(fname('cache', 'file0.png')) + assert not os.path.exists(fname('cache', 'file1.png')) + assert not os.path.exists(fname('cache', 'file2.png')) + assert not os.path.exists(fname('cache', 'file3.png')) + assert not os.path.exists(fname('cache', 'file4.png')) + assert not os.path.exists(fname('cache', 'onemore.png')) def test_cache_default_dir(): @@ -96,10 +89,7 @@ def test_cache_default_dir(): side_effect=OSError) def test_cache_mkdir_error(mkdirs, tmpdir): with pytest.raises(_CacheError): - try: - c = _ConversionCache(str(tmpdir.join('cache'))) - finally: - tmpdir.remove(rec=1) + c = _ConversionCache(str(tmpdir.join('cache'))) @mock.patch('matplotlib.testing._conversion_cache.os.access', @@ -108,7 +98,4 @@ def test_cache_unwritable_error(access, tmpdir): with pytest.raises(_CacheError): cachedir = tmpdir.join('cache') cachedir.ensure(dir=True) - try: - c = _ConversionCache(str(cachedir)) - finally: - tmpdir.remove(rec=1) + c = _ConversionCache(str(cachedir)) From f47cbe4314ae001b08a289c9965c427b6b80f7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 22 Feb 2017 11:40:33 +0200 Subject: [PATCH 18/19] Move pytest_addoption call to top-level conftest.py It only works there, see https://github.com/pytest-dev/pytest/issues/1427 --- conftest.py | 11 +++++++++++ lib/matplotlib/sphinxext/tests/conftest.py | 2 +- lib/matplotlib/testing/conftest.py | 9 --------- lib/matplotlib/tests/conftest.py | 2 +- lib/mpl_toolkits/tests/conftest.py | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000000..8d399e677fed --- /dev/null +++ b/conftest.py @@ -0,0 +1,11 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + + +def pytest_addoption(parser): + group = parser.getgroup("matplotlib", "matplotlib custom options") + group.addoption("--conversion-cache-max-size", action="store", + help="conversion cache maximum size in bytes") + group.addoption("--conversion-cache-report-misses", + action="store_true", + help="report conversion cache misses") diff --git a/lib/matplotlib/sphinxext/tests/conftest.py b/lib/matplotlib/sphinxext/tests/conftest.py index c92a63562827..0e02d385d2ee 100644 --- a/lib/matplotlib/sphinxext/tests/conftest.py +++ b/lib/matplotlib/sphinxext/tests/conftest.py @@ -2,5 +2,5 @@ unicode_literals) from matplotlib.testing.conftest import ( - mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + mpl_test_settings, pytest_configure, pytest_unconfigure, pytest_sessionfinish, pytest_terminal_summary) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index e01078ffe3f1..2725f99e091e 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -7,15 +7,6 @@ from matplotlib.testing import _conversion_cache as ccache -def pytest_addoption(parser): - group = parser.getgroup("matplotlib", "matplotlib custom options") - group.addoption("--conversion-cache-max-size", action="store", - help="conversion cache maximum size in bytes") - group.addoption("--conversion-cache-report-misses", - action="store_true", - help="report conversion cache misses") - - def pytest_configure(config): matplotlib.use('agg') matplotlib._called_from_pytest = True diff --git a/lib/matplotlib/tests/conftest.py b/lib/matplotlib/tests/conftest.py index de366f7bffa1..5fb379099914 100644 --- a/lib/matplotlib/tests/conftest.py +++ b/lib/matplotlib/tests/conftest.py @@ -2,6 +2,6 @@ unicode_literals) from matplotlib.testing.conftest import ( - mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + mpl_test_settings, pytest_configure, pytest_unconfigure, pytest_sessionfinish, pytest_terminal_summary) diff --git a/lib/mpl_toolkits/tests/conftest.py b/lib/mpl_toolkits/tests/conftest.py index c92a63562827..0e02d385d2ee 100644 --- a/lib/mpl_toolkits/tests/conftest.py +++ b/lib/mpl_toolkits/tests/conftest.py @@ -2,5 +2,5 @@ unicode_literals) from matplotlib.testing.conftest import ( - mpl_test_settings, pytest_addoption, pytest_configure, pytest_unconfigure, + mpl_test_settings, pytest_configure, pytest_unconfigure, pytest_sessionfinish, pytest_terminal_summary) From 996f22b90276472257ccd405b35645bfff855f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Wed, 22 Feb 2017 11:53:53 +0200 Subject: [PATCH 19/19] pep8 fix --- lib/matplotlib/tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/tests/conftest.py b/lib/matplotlib/tests/conftest.py index 5fb379099914..0e02d385d2ee 100644 --- a/lib/matplotlib/tests/conftest.py +++ b/lib/matplotlib/tests/conftest.py @@ -4,4 +4,3 @@ from matplotlib.testing.conftest import ( mpl_test_settings, pytest_configure, pytest_unconfigure, pytest_sessionfinish, pytest_terminal_summary) -