Skip to content

Simplify cleanup decorator implementation. #11292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
4 changes: 4 additions & 0 deletions doc/api/next_api_changes/2018-05-22-AL.rst
Original file line number Diff line number Diff line change
@@ -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``.
10 changes: 6 additions & 4 deletions lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import matplotlib as mpl
from matplotlib import cbook
from matplotlib.cbook import MatplotlibDeprecationWarning


def is_called_from_pytest():
Expand Down Expand Up @@ -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()
67 changes: 35 additions & 32 deletions lib/matplotlib/testing/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import warnings

import pytest

import matplotlib
from matplotlib import cbook
from matplotlib.cbook import MatplotlibDeprecationWarning


def pytest_configure(config):
Expand All @@ -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
Expand Down
90 changes: 33 additions & 57 deletions lib/matplotlib/testing/decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
from distutils.version import StrictVersion
import functools
import inspect
Expand All @@ -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):
Expand All @@ -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

Expand Down