From ffcbe0a7121c810408920c29b2d88b30fee71c9e Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Wed, 22 Nov 2017 09:25:39 -0500 Subject: [PATCH] TST: Skipif decorator for matplotlib #18190 --- pandas/tests/io/formats/test_style.py | 3 +- pandas/tests/plotting/common.py | 4 +- pandas/tests/plotting/test_boxplot_method.py | 5 +- pandas/tests/plotting/test_datetimelike.py | 4 +- pandas/tests/plotting/test_deprecated.py | 4 +- pandas/tests/plotting/test_frame.py | 4 +- pandas/tests/plotting/test_groupby.py | 4 +- pandas/tests/plotting/test_hist_method.py | 7 +- pandas/tests/plotting/test_misc.py | 5 +- pandas/tests/plotting/test_series.py | 4 +- pandas/tests/test_resample.py | 4 +- pandas/tests/util/test_util.py | 18 +++++ pandas/util/_test_decorators.py | 71 ++++++++++++++++++++ pandas/util/testing.py | 7 -- 14 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 pandas/util/_test_decorators.py diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 0160371dc413d..4b0ca872da326 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -7,6 +7,7 @@ import pandas as pd from pandas import DataFrame import pandas.util.testing as tm +import pandas.util._test_decorators as td jinja2 = pytest.importorskip('jinja2') from pandas.io.formats.style import Styler, _get_level_lengths # noqa @@ -1011,8 +1012,8 @@ def test_hide_columns_mult_levels(self): class TestStylerMatplotlibDep(object): + @td.skip_if_no_mpl def test_background_gradient(self): - tm._skip_if_no_mpl() df = pd.DataFrame([[1, 2], [2, 4]], columns=['A', 'B']) for c_map in [None, 'YlOrRd']: diff --git a/pandas/tests/plotting/common.py b/pandas/tests/plotting/common.py index dfab539e9474c..2e62b22b2b69e 100644 --- a/pandas/tests/plotting/common.py +++ b/pandas/tests/plotting/common.py @@ -12,6 +12,7 @@ import pandas.util.testing as tm from pandas.util.testing import (ensure_clean, assert_is_valid_plot_return_object) +import pandas.util._test_decorators as td import numpy as np from numpy import random @@ -23,8 +24,6 @@ This is a common base class used for various plotting tests """ -tm._skip_if_no_mpl() - def _skip_if_no_scipy_gaussian_kde(): try: @@ -43,6 +42,7 @@ def _ok_for_gaussian_kde(kind): return plotting._compat._mpl_ge_1_5_0() +@td.skip_if_no_mpl class TestPlotBase(object): def setup_method(self, method): diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index 4b1cb2ccbd3dd..1bc49e9e5f96a 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -8,6 +8,7 @@ from pandas import Series, DataFrame, MultiIndex from pandas.compat import range, lzip import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from numpy import random @@ -19,8 +20,6 @@ """ Test cases for .boxplot method """ -tm._skip_if_no_mpl() - def _skip_if_mpl_14_or_dev_boxplot(): # GH 8382 @@ -31,6 +30,7 @@ def _skip_if_mpl_14_or_dev_boxplot(): pytest.skip("Matplotlib Regression in 1.4 and current dev.") +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): @pytest.mark.slow @@ -174,6 +174,7 @@ def test_fontsize(self): xlabelsize=16, ylabelsize=16) +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): @pytest.mark.slow diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index d66012e2a56a0..f1a478581e730 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -16,13 +16,13 @@ from pandas.util.testing import assert_series_equal, ensure_clean import pandas.util.testing as tm +import pandas.util._test_decorators as td from pandas.tests.plotting.common import (TestPlotBase, _skip_if_no_scipy_gaussian_kde) -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestTSPlot(TestPlotBase): def setup_method(self, method): diff --git a/pandas/tests/plotting/test_deprecated.py b/pandas/tests/plotting/test_deprecated.py index 970de6ff881ab..d2f8e13a2444b 100644 --- a/pandas/tests/plotting/test_deprecated.py +++ b/pandas/tests/plotting/test_deprecated.py @@ -4,6 +4,7 @@ import pandas as pd import pandas.util.testing as tm +import pandas.util._test_decorators as td import pytest from numpy.random import randn @@ -18,9 +19,8 @@ pandas.tools.plotting """ -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestDeprecatedNameSpace(TestPlotBase): @pytest.mark.slow diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 3887271edb2a3..5c72d778a1220 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -15,6 +15,7 @@ from pandas.compat import range, lrange, lmap, lzip, u, zip, PY3 from pandas.io.formats.printing import pprint_thing import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from numpy.random import rand, randn @@ -24,9 +25,8 @@ _skip_if_no_scipy_gaussian_kde, _ok_for_gaussian_kde) -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): def setup_method(self, method): diff --git a/pandas/tests/plotting/test_groupby.py b/pandas/tests/plotting/test_groupby.py index de48b58133e9a..a7c99a06c34e9 100644 --- a/pandas/tests/plotting/test_groupby.py +++ b/pandas/tests/plotting/test_groupby.py @@ -5,14 +5,14 @@ from pandas import Series, DataFrame import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from pandas.tests.plotting.common import TestPlotBase -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): def test_series_groupby_plotting_nominally_works(self): diff --git a/pandas/tests/plotting/test_hist_method.py b/pandas/tests/plotting/test_hist_method.py index 5f7b2dd2d6ca9..864d39eba29c5 100644 --- a/pandas/tests/plotting/test_hist_method.py +++ b/pandas/tests/plotting/test_hist_method.py @@ -6,6 +6,7 @@ from pandas import Series, DataFrame import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from numpy.random import randn @@ -14,9 +15,7 @@ from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works) -tm._skip_if_no_mpl() - - +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): def setup_method(self, method): @@ -141,6 +140,7 @@ def test_plot_fails_when_ax_differs_from_figure(self): self.ts.hist(ax=ax1, figure=fig2) +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): @pytest.mark.slow @@ -251,6 +251,7 @@ def test_tight_layout(self): tm.close() +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): @pytest.mark.slow diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index 6f476553091d9..8b0a981760c72 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -7,6 +7,7 @@ from pandas import DataFrame from pandas.compat import lmap import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from numpy import random @@ -15,9 +16,8 @@ import pandas.plotting as plotting from pandas.tests.plotting.common import TestPlotBase, _check_plot_works -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): def setup_method(self, method): @@ -49,6 +49,7 @@ def test_bootstrap_plot(self): _check_plot_works(bootstrap_plot, series=self.ts, size=10) +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): def test_scatter_matrix_axis(self): diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index fdfd87d1e898c..6dd7e1e9882b2 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -12,6 +12,7 @@ from pandas import Series, DataFrame, date_range from pandas.compat import range, lrange import pandas.util.testing as tm +import pandas.util._test_decorators as td import numpy as np from numpy.random import randn @@ -21,9 +22,8 @@ _skip_if_no_scipy_gaussian_kde, _ok_for_gaussian_kde) -tm._skip_if_no_mpl() - +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): def setup_method(self, method): diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index bf1cac3112c46..b0154f6db7022 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -13,6 +13,7 @@ import pandas as pd import pandas.tseries.offsets as offsets import pandas.util.testing as tm +import pandas.util._test_decorators as td from pandas import (Series, DataFrame, Panel, Index, isna, notna, Timestamp) @@ -234,9 +235,8 @@ def test_groupby_resample_on_api(self): result = df.groupby('key').resample('D', on='dates').mean() assert_frame_equal(result, expected) + @td.skip_if_no_mpl def test_plot_api(self): - tm._skip_if_no_mpl() - # .resample(....).plot(...) # hitting warnings # GH 12448 diff --git a/pandas/tests/util/test_util.py b/pandas/tests/util/test_util.py index 659ce36de6bab..be4e60c6493c8 100644 --- a/pandas/tests/util/test_util.py +++ b/pandas/tests/util/test_util.py @@ -16,6 +16,7 @@ validate_bool_kwarg) import pandas.util.testing as tm +from pandas.util._test_decorators import safe_import class TestDecorators(object): @@ -482,3 +483,20 @@ def test_make_signature(): assert sig == (['old_arg_name', 'new_arg_name', 'mapping=None', 'stacklevel=2'], ['old_arg_name', 'new_arg_name', 'mapping', 'stacklevel']) + + +def test_safe_import(monkeypatch): + assert not safe_import("foo") + assert not safe_import("pandas", min_version="99.99.99") + + # Create dummy module to be imported + import types + import sys + mod_name = "hello123" + mod = types.ModuleType(mod_name) + mod.__version__ = "1.5" + + assert not safe_import(mod_name) + monkeypatch.setitem(sys.modules, mod_name, mod) + assert not safe_import(mod_name, min_version="2.0") + assert safe_import(mod_name, min_version="1.0") diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py new file mode 100644 index 0000000000000..b592a73e5d758 --- /dev/null +++ b/pandas/util/_test_decorators.py @@ -0,0 +1,71 @@ +""" +This module provides decorator functions which can be applied to test objects +in order to skip those objects when certain conditions occur. A sample use case +is to detect if the platform is missing ``matplotlib``. If so, any test objects +which require ``matplotlib`` and decorated with ``@td.skip_if_no_mpl`` will be +skipped by ``pytest`` during the execution of the test suite. + +To illustrate, after importing this module: + +import pandas.util._test_decorators as td + +The decorators can be applied to classes: + +@td.skip_if_some_reason +class Foo(): + ... + +Or individual functions: + +@td.skip_if_some_reason +def test_foo(): + ... + +For more information, refer to the ``pytest`` documentation on ``skipif``. +""" + +import pytest + + +def safe_import(mod_name, min_version=None): + """ + Parameters: + ----------- + mod_name : str + Name of the module to be imported + min_version : str, default None + Minimum required version of the specified mod_name + + Returns: + -------- + object + The imported module if successful, or False + """ + try: + mod = __import__(mod_name) + except ImportError: + return False + + if not min_version: + return mod + else: + import sys + version = getattr(sys.modules[mod_name], '__version__') + if version: + from distutils.version import LooseVersion + if LooseVersion(version) >= LooseVersion(min_version): + return mod + + return False + + +def _skip_if_no_mpl(): + mod = safe_import("matplotlib") + if mod: + mod.use("Agg", warn=False) + else: + return True + + +skip_if_no_mpl = pytest.mark.skipif(_skip_if_no_mpl(), + reason="Missing matplotlib dependency") diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 0da59ba5f958e..ff6fa8ae717d3 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -325,13 +325,6 @@ def _skip_if_32bit(): pytest.skip("skipping for 32 bit") -def _skip_if_no_mpl(): - import pytest - - mpl = pytest.importorskip("matplotlib") - mpl.use("Agg", warn=False) - - def _skip_if_mpl_1_5(): import matplotlib as mpl