Repository: pandas-dev/pandas

Issue:
TST: make _skip_if into pytest decorators
- [X] _skip_if_32bit (#18693)
- [X] _skip_if_no_mpl (#18427)
- [X] _skip_if_mpl_1_5 (#18682)
- [x] _skip_if_no_scipy (#18794)
- [x] _skip_if_no_lzma (#18820)
- [x] _skip_if_no_xarray (#18814)
- [X] _skip_if_windows_python_3 (#18693)
- [X] _skip_if_windows (#18693)
- [x] _skip_if_no_pathlib (#18765) 
- [x] _skip_if_no_localpath (#18765)
- [x] skip_if_no_ne (#18820)
- [x] _skip_if_has_locale (#18745) 
- [x] _skip_if_not_us_locale (#18745)
- [ ] _skip_if_no_mock
- [x] _skip_if_no_ipython (#18814)
- [ ] skip_if_no_package

we should move the ``_skip_if_*`` functions out of ``pandas.util.testing`` to another (private module)

then we can add [skipif decorators](http://pytest.readthedocs.io/en/reorganize-docs/new-docs/user/skipping.html)

and use like this

```
@skip_if_windows_py3
def test_.......():
```

rather than calling ``tm._skip_if_windows_py390`` in the body of the function (sometimes you also need to do that, so we leave the functions themselves as well).

this makes much more idiomatic and readable pytest code and removes the need to roll your own when using the decorator.



Discussion:
I could take a stab at this. What private module do you want the decorators to be in? Something like `_test_decorators.py` to enable `from pandas.util._test_decorators import *`?
i wouldn’t import * them but the name is ok 
For the skipif decorators, we need to provide a function that evaluates to ``True`` to get it to skip whatever is being wrapped. A lot of the pandas-internal skip functions use pytest's ``importorskip``, which raises a ``Skipped`` when the required module cannot be imported.

I would propose here to catch a ``Skipped`` in the decorator function and return ``True`` if that occurs. Sample code is provided below to illustrate for mpl.

Before I go too far - do you agree with this direction? I'm somewhat hesitant to use it given the ``Skipped`` exception needs to be imported from ``_pytest``. However, the alternative would be to refactor the existing functions to not use importorskip, which has its own downsides. FWIW if we wanted to go with the try...except methodology I'd create a wrapper function internal to the _test_decorators module to handle that rather than coding that block for each decorator that we want to create

    import pytest
    from _pytest.outcomes import Skipped

    def _skip_if_no_mpl():
        try:
            mpl = pytest.importorskip("matplotlib")
            mpl.use("Agg", warn=False)
        except Skipped:
            return True
    
    skip_if_no_mpl = pytest.mark.skipif(_skip_if_no_mpl(), reason="No matplotlib")

I would refactor like your 2nd method, IOW have a safe importer that returns a boolean, then you could actually auto-create the functions & skip decorators.
certainly if you want a PR for a simple version would be great to see before you convert everything.

key is that *has* to be a separate module that is *only* imported when tests are run and not in pandas import (so deps are not imported until then)
EDIT - moved checklist to top of thread
@jreback as far as the ``max_version`` argument goes in the current testing module's ``skip_if_no_package`` function I only since one usage in the following location:

https://github.com/pandas-dev/pandas/blob/7a0ee19e95e29042c3373a1d912d2db27108c828/pandas/tests/sparse/test_frame.py#L1234

I believe that was introduced by 56b5a30937e79335029b1727a80f109b7eb0840a and I looked through the related comments in #15668 but am not clear on why that particular test needs the v <= 0.19.0 criteria. Any insight as to if that is needed or not?

FWIW that test runs on my machine with SciPy 1.0, save the ``dok_matrix`` fixture which may have an unrelated issue.
@WillAyd I updated the top list, but I think I missed a couple.

Resolving patch:
diff --git a/pandas/conftest.py b/pandas/conftest.py
--- a/pandas/conftest.py
+++ b/pandas/conftest.py
@@ -3,7 +3,6 @@
 from distutils.version import LooseVersion
 import numpy
 import pandas
-import pandas.util.testing as tm
 import dateutil
 
 
@@ -51,7 +50,6 @@ def add_imports(doctest_namespace):
 
 @pytest.fixture(params=['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil'])
 def spmatrix(request):
-    tm._skip_if_no_scipy()
     from scipy import sparse
     return getattr(sparse, request.param + '_matrix')
 


Test patch:
diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py
--- a/pandas/tests/io/test_pytables.py
+++ b/pandas/tests/io/test_pytables.py
@@ -718,8 +718,6 @@ def test_put_compression(self):
 
     @td.skip_if_windows_python_3
     def test_put_compression_blosc(self):
-        tm.skip_if_no_package('tables', min_version='2.2',
-                              app='blosc support')
         df = tm.makeTimeDataFrame()
 
         with ensure_clean_store(self.path) as store:
diff --git a/pandas/tests/sparse/test_frame.py b/pandas/tests/sparse/test_frame.py
--- a/pandas/tests/sparse/test_frame.py
+++ b/pandas/tests/sparse/test_frame.py
@@ -7,6 +7,7 @@
 from numpy import nan
 import numpy as np
 import pandas as pd
+from distutils.version import LooseVersion
 
 from pandas import Series, DataFrame, bdate_range, Panel
 from pandas.core.dtypes.common import (
@@ -20,6 +21,7 @@
 from pandas.compat import lrange
 from pandas import compat
 from pandas.core.sparse import frame as spf
+import pandas.util._test_decorators as td
 
 from pandas._libs.sparse import BlockIndex, IntIndex
 from pandas.core.sparse.api import SparseSeries, SparseDataFrame, SparseArray
@@ -1169,14 +1171,13 @@ def test_notna(self):
         tm.assert_frame_equal(res.to_dense(), exp)
 
 
+@td.skip_if_no_scipy
 @pytest.mark.parametrize('index', [None, list('abc')])  # noqa: F811
 @pytest.mark.parametrize('columns', [None, list('def')])
 @pytest.mark.parametrize('fill_value', [None, 0, np.nan])
 @pytest.mark.parametrize('dtype', [bool, int, float, np.uint16])
 def test_from_to_scipy(spmatrix, index, columns, fill_value, dtype):
     # GH 4343
-    tm.skip_if_no_package('scipy')
-
     # Make one ndarray and from it one sparse matrix, both to be used for
     # constructing frames and comparing results
     arr = np.eye(3, dtype=dtype)
@@ -1225,13 +1226,17 @@ def test_from_to_scipy(spmatrix, index, columns, fill_value, dtype):
     assert sdf.to_coo().dtype == np.object_
 
 
+@td.skip_if_no_scipy
 @pytest.mark.parametrize('fill_value', [None, 0, np.nan])  # noqa: F811
 def test_from_to_scipy_object(spmatrix, fill_value):
     # GH 4343
     dtype = object
     columns = list('cd')
     index = list('ab')
-    tm.skip_if_no_package('scipy', max_version='0.19.0')
+    import scipy
+    if (spmatrix is scipy.sparse.dok_matrix and LooseVersion(
+            scipy.__version__) >= LooseVersion('0.19.0')):
+        pytest.skip("dok_matrix from object does not work in SciPy >= 0.19")
 
     # Make one ndarray and from it one sparse matrix, both to be used for
     # constructing frames and comparing results
@@ -1270,10 +1275,9 @@ def test_from_to_scipy_object(spmatrix, fill_value):
     assert sdf.to_coo().dtype == res_dtype
 
 
+@td.skip_if_no_scipy
 def test_from_scipy_correct_ordering(spmatrix):
     # GH 16179
-    tm.skip_if_no_package('scipy')
-
     arr = np.arange(1, 5).reshape(2, 2)
     try:
         spm = spmatrix(arr)
@@ -1290,10 +1294,9 @@ def test_from_scipy_correct_ordering(spmatrix):
     tm.assert_frame_equal(sdf.to_dense(), expected.to_dense())
 
 
+@td.skip_if_no_scipy
 def test_from_scipy_fillna(spmatrix):
     # GH 16112
-    tm.skip_if_no_package('scipy')
-
     arr = np.eye(3)
     arr[1:, 0] = np.nan
 
diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py
--- a/pandas/tests/test_nanops.py
+++ b/pandas/tests/test_nanops.py
@@ -12,6 +12,7 @@
 from pandas.core.dtypes.common import is_integer_dtype
 import pandas.core.nanops as nanops
 import pandas.util.testing as tm
+import pandas.util._test_decorators as td
 
 use_bn = nanops._USE_BOTTLENECK
 
@@ -381,8 +382,8 @@ def test_nanstd(self):
                              allow_str=False, allow_date=False,
                              allow_tdelta=True, allow_obj='convert')
 
+    @td.skip_if_no('scipy', min_version='0.17.0')
     def test_nansem(self):
-        tm.skip_if_no_package('scipy', min_version='0.17.0')
         from scipy.stats import sem
         with np.errstate(invalid='ignore'):
             self.check_funs_ddof(nanops.nansem, sem, allow_complex=False,
@@ -441,8 +442,8 @@ def _skew_kurt_wrap(self, values, axis=None, func=None):
             return 0.
         return result
 
+    @td.skip_if_no('scipy', min_version='0.17.0')
     def test_nanskew(self):
-        tm.skip_if_no_package('scipy', min_version='0.17.0')
         from scipy.stats import skew
         func = partial(self._skew_kurt_wrap, func=skew)
         with np.errstate(invalid='ignore'):
@@ -450,8 +451,8 @@ def test_nanskew(self):
                             allow_str=False, allow_date=False,
                             allow_tdelta=False)
 
+    @td.skip_if_no('scipy', min_version='0.17.0')
     def test_nankurt(self):
-        tm.skip_if_no_package('scipy', min_version='0.17.0')
         from scipy.stats import kurtosis
         func1 = partial(kurtosis, fisher=True)
         func = partial(self._skew_kurt_wrap, func=func1)
@@ -549,8 +550,8 @@ def test_nancorr_pearson(self):
         self.check_nancorr_nancov_1d(nanops.nancorr, targ0, targ1,
                                      method='pearson')
 
+    @td.skip_if_no_scipy
     def test_nancorr_kendall(self):
-        tm.skip_if_no_package('scipy.stats')
         from scipy.stats import kendalltau
         targ0 = kendalltau(self.arr_float_2d, self.arr_float1_2d)[0]
         targ1 = kendalltau(self.arr_float_2d.flat, self.arr_float1_2d.flat)[0]
@@ -561,8 +562,8 @@ def test_nancorr_kendall(self):
         self.check_nancorr_nancov_1d(nanops.nancorr, targ0, targ1,
                                      method='kendall')
 
+    @td.skip_if_no_scipy
     def test_nancorr_spearman(self):
-        tm.skip_if_no_package('scipy.stats')
         from scipy.stats import spearmanr
         targ0 = spearmanr(self.arr_float_2d, self.arr_float1_2d)[0]
         targ1 = spearmanr(self.arr_float_2d.flat, self.arr_float1_2d.flat)[0]
diff --git a/pandas/util/testing.py b/pandas/util/testing.py
--- a/pandas/util/testing.py
+++ b/pandas/util/testing.py
@@ -15,7 +15,6 @@
 from datetime import datetime
 from functools import wraps, partial
 from contextlib import contextmanager
-from distutils.version import LooseVersion
 
 from numpy.random import randn, rand
 import numpy as np
@@ -317,35 +316,6 @@ def close(fignum=None):
         _close(fignum)
 
 
-def _skip_if_mpl_1_5():
-    import matplotlib as mpl
-
-    v = mpl.__version__
-    if LooseVersion(v) > LooseVersion('1.4.3') or str(v)[0] == '0':
-        import pytest
-        pytest.skip("matplotlib 1.5")
-    else:
-        mpl.use("Agg", warn=False)
-
-
-def _skip_if_no_scipy():
-    import pytest
-
-    pytest.importorskip("scipy.stats")
-    pytest.importorskip("scipy.sparse")
-    pytest.importorskip("scipy.interpolate")
-
-
-def _skip_if_no_mock():
-    try:
-        import mock  # noqa
-    except ImportError:
-        try:
-            from unittest import mock  # noqa
-        except ImportError:
-            import pytest
-            raise pytest.skip("mock is not installed")
-
 # -----------------------------------------------------------------------------
 # locale utilities
 
@@ -1979,62 +1949,6 @@ def __init__(self, *args, **kwargs):
         dict.__init__(self, *args, **kwargs)
 
 
-# Dependency checker when running tests.
-#
-# Copied this from nipy/nipype
-# Copyright of respective developers, License: BSD-3
-def skip_if_no_package(pkg_name, min_version=None, max_version=None,
-                       app='pandas', checker=LooseVersion):
-    """Check that the min/max version of the required package is installed.
-
-    If the package check fails, the test is automatically skipped.
-
-    Parameters
-    ----------
-    pkg_name : string
-        Name of the required package.
-    min_version : string, optional
-        Minimal version number for required package.
-    max_version : string, optional
-        Max version number for required package.
-    app : string, optional
-        Application that is performing the check. For instance, the
-        name of the tutorial being executed that depends on specific
-        packages.
-    checker : object, optional
-        The class that will perform the version checking. Default is
-        distutils.version.LooseVersion.
-
-    Examples
-    --------
-    package_check('numpy', '1.3')
-
-    """
-
-    import pytest
-    if app:
-        msg = '{app} requires {pkg_name}'.format(app=app, pkg_name=pkg_name)
-    else:
-        msg = 'module requires {pkg_name}'.format(pkg_name=pkg_name)
-    if min_version:
-        msg += ' with version >= {min_version}'.format(min_version=min_version)
-    if max_version:
-        msg += ' with version < {max_version}'.format(max_version=max_version)
-    try:
-        mod = __import__(pkg_name)
-    except ImportError:
-        mod = None
-    try:
-        have_version = mod.__version__
-    except AttributeError:
-        pytest.skip('Cannot find version for {pkg_name}'
-                    .format(pkg_name=pkg_name))
-    if min_version and checker(have_version) < checker(min_version):
-        pytest.skip(msg)
-    if max_version and checker(have_version) >= checker(max_version):
-        pytest.skip(msg)
-
-
 def optional_args(decorator):
     """allows a decorator to take optional positional and keyword arguments.
     Assumes that taking a single, callable, positional argument means that