diff --git a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst index 891dcedf4439..2475b736de78 100644 --- a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst +++ b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst @@ -25,7 +25,8 @@ The following classes, methods, functions, and attributes are deprecated: - ``mathtext.unichr_safe`` (use ``chr`` instead), - ``table.Table.get_child_artists`` (use ``get_children`` instead), - ``testing.compare.ImageComparisonTest``, ``testing.compare.compare_float``, -- ``testing.decorators.skip_if_command_unavailable``. +- ``testing.decorators.CleanupTest``, + ``testing.decorators.skip_if_command_unavailable``, - ``FigureCanvasQT.keyAutoRepeat`` (directly check ``event.guiEvent.isAutoRepeat()`` in the event handler to decide whether to handle autorepeated key presses). diff --git a/doc/api/next_api_changes/2018-05-22-AL.rst b/doc/api/next_api_changes/2018-05-22-AL.rst new file mode 100644 index 000000000000..1c8b7adcdf28 --- /dev/null +++ b/doc/api/next_api_changes/2018-05-22-AL.rst @@ -0,0 +1,4 @@ +The cleanup decorators and test classes in matplotlib.testing.decorators no longer destroy the warnings filter on exit +`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` +Instead, they restore the warnings filter that existed before the test started +using ``warnings.catch_warnings``. diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index e9af802daa70..ffc2c7fee03c 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -3,6 +3,7 @@ import matplotlib as mpl from matplotlib import cbook +from matplotlib.cbook import MatplotlibDeprecationWarning def is_called_from_pytest(): @@ -38,10 +39,11 @@ def setup(): mpl.use('Agg', warn=False) # use Agg backend for these tests - # These settings *must* be hardcoded for running the comparison - # tests and are not necessarily the default values as specified in - # rcsetup.py - mpl.rcdefaults() # Start with all defaults + # These settings *must* be hardcoded for running the comparison tests and + # are not necessarily the default values as specified in rcsetup.py + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MatplotlibDeprecationWarning) + mpl.rcdefaults() # Start with all defaults set_font_settings_for_testing() set_reproducibility_for_testing() diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 3d609f15d432..5f2b2ca5a63b 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -1,7 +1,10 @@ +import warnings + import pytest import matplotlib from matplotlib import cbook +from matplotlib.cbook import MatplotlibDeprecationWarning def pytest_configure(config): @@ -16,39 +19,39 @@ def pytest_unconfigure(config): @pytest.fixture(autouse=True) def mpl_test_settings(request): - from matplotlib.testing.decorators import _do_cleanup - - original_units_registry = matplotlib.units.registry.copy() - original_settings = matplotlib.rcParams.copy() - - backend = None - backend_marker = request.keywords.get('backend') - if backend_marker is not None: - assert len(backend_marker.args) == 1, \ - "Marker 'backend' must specify 1 backend." - backend = backend_marker.args[0] - prev_backend = matplotlib.get_backend() - - style = '_classic_test' # Default of cleanup and image_comparison too. - style_marker = request.keywords.get('style') - if style_marker is not None: - assert len(style_marker.args) == 1, \ - "Marker 'style' must specify 1 style." - style = style_marker.args[0] - - matplotlib.testing.setup() - if backend is not None: - # This import must come after setup() so it doesn't load the default - # backend prematurely. - import matplotlib.pyplot as plt - plt.switch_backend(backend) - matplotlib.style.use(style) - try: - yield - finally: + from matplotlib.testing.decorators import _cleanup_cm + + with _cleanup_cm(): + + backend = None + backend_marker = request.keywords.get('backend') + if backend_marker is not None: + assert len(backend_marker.args) == 1, \ + "Marker 'backend' must specify 1 backend." + backend = backend_marker.args[0] + prev_backend = matplotlib.get_backend() + + style = '_classic_test' # Default of cleanup and image_comparison too. + style_marker = request.keywords.get('style') + if style_marker is not None: + assert len(style_marker.args) == 1, \ + "Marker 'style' must specify 1 style." + style = style_marker.args[0] + + matplotlib.testing.setup() if backend is not None: - plt.switch_backend(prev_backend) - _do_cleanup(original_units_registry, original_settings) + # This import must come after setup() so it doesn't load the + # default backend prematurely. + import matplotlib.pyplot as plt + plt.switch_backend(backend) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", MatplotlibDeprecationWarning) + matplotlib.style.use(style) + try: + yield + finally: + if backend is not None: + plt.switch_backend(prev_backend) @pytest.fixture diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 4bf5d081d9cd..97fcf23a4cb7 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -1,3 +1,4 @@ +import contextlib from distutils.version import StrictVersion import functools import inspect @@ -8,63 +9,49 @@ import unittest import warnings -# Note - don't import nose up here - import it only as needed in functions. -# This allows other functions here to be used by pytest-based testing suites -# without requiring nose to be installed. - - import matplotlib as mpl import matplotlib.style import matplotlib.units import matplotlib.testing from matplotlib import cbook -from matplotlib import ticker -from matplotlib import pyplot as plt from matplotlib import ft2font -from matplotlib.testing.compare import ( - comparable_formats, compare_images, make_test_filename) +from matplotlib import pyplot as plt +from matplotlib import ticker from . import is_called_from_pytest +from .compare import comparable_formats, compare_images, make_test_filename from .exceptions import ImageComparisonFailure -def _do_cleanup(original_units_registry, original_settings): - plt.close('all') - - mpl.rcParams.clear() - mpl.rcParams.update(original_settings) - matplotlib.units.registry.clear() - matplotlib.units.registry.update(original_units_registry) - warnings.resetwarnings() # reset any warning filters set in tests - - -class CleanupTest(object): - @classmethod - def setup_class(cls): - cls.original_units_registry = matplotlib.units.registry.copy() - cls.original_settings = mpl.rcParams.copy() - matplotlib.testing.setup() - - @classmethod - def teardown_class(cls): - _do_cleanup(cls.original_units_registry, - cls.original_settings) - - def test(self): - self._func() +@contextlib.contextmanager +def _cleanup_cm(): + orig_units_registry = matplotlib.units.registry.copy() + try: + with warnings.catch_warnings(), matplotlib.rc_context(): + yield + finally: + matplotlib.units.registry.clear() + matplotlib.units.registry.update(orig_units_registry) + plt.close("all") class CleanupTestCase(unittest.TestCase): - '''A wrapper for unittest.TestCase that includes cleanup operations''' + """A wrapper for unittest.TestCase that includes cleanup operations.""" @classmethod def setUpClass(cls): - import matplotlib.units - cls.original_units_registry = matplotlib.units.registry.copy() - cls.original_settings = mpl.rcParams.copy() + cls._cm = _cleanup_cm().__enter__() @classmethod def tearDownClass(cls): - _do_cleanup(cls.original_units_registry, - cls.original_settings) + cls._cm.__exit__(None, None, None) + + +@cbook.deprecated("3.0") +class CleanupTest(object): + setup_class = classmethod(CleanupTestCase.setUpClass.__func__) + teardown_class = classmethod(CleanupTestCase.tearDownClass.__func__) + + def test(self): + self._func() def cleanup(style=None): @@ -78,34 +65,23 @@ def cleanup(style=None): The name of the style to apply. """ - # If cleanup is used without arguments, `style` will be a - # callable, and we pass it directly to the wrapper generator. If - # cleanup if called with an argument, it is a string naming a - # style, and the function will be passed as an argument to what we - # return. This is a confusing, but somewhat standard, pattern for - # writing a decorator with optional arguments. + # If cleanup is used without arguments, `style` will be a callable, and we + # pass it directly to the wrapper generator. If cleanup if called with an + # argument, it is a string naming a style, and the function will be passed + # as an argument to what we return. This is a confusing, but somewhat + # standard, pattern for writing a decorator with optional arguments. def make_cleanup(func): if inspect.isgeneratorfunction(func): @functools.wraps(func) def wrapped_callable(*args, **kwargs): - original_units_registry = matplotlib.units.registry.copy() - original_settings = mpl.rcParams.copy() - matplotlib.style.use(style) - try: + with _cleanup_cm(), matplotlib.style.context(style): yield from func(*args, **kwargs) - finally: - _do_cleanup(original_units_registry, original_settings) else: @functools.wraps(func) def wrapped_callable(*args, **kwargs): - original_units_registry = matplotlib.units.registry.copy() - original_settings = mpl.rcParams.copy() - matplotlib.style.use(style) - try: + with _cleanup_cm(), matplotlib.style.context(style): func(*args, **kwargs) - finally: - _do_cleanup(original_units_registry, original_settings) return wrapped_callable