Repository: pandas-dev/pandas

Issue:
Clean up code duplication in _optional and _print_versions
ref this comment https://github.com/pandas-dev/pandas/pull/26802#discussion_r293045993 we currently duplicate code in managing versions between the pandas.compat._optional and pandas.util._print_versions modules which we should align


Discussion:
Taking a look now.

Resolving patch:
diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py
--- a/pandas/compat/_optional.py
+++ b/pandas/compat/_optional.py
@@ -19,6 +19,7 @@
     "s3fs": "0.0.8",
     "scipy": "0.19.0",
     "sqlalchemy": "1.1.4",
+    "tables": "3.4.2",
     "xarray": "0.8.2",
     "xlrd": "1.1.0",
     "xlwt": "1.2.0",
diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py
--- a/pandas/io/pytables.py
+++ b/pandas/io/pytables.py
@@ -19,6 +19,7 @@
 
 from pandas._libs import lib, writers as libwriters
 from pandas._libs.tslibs import timezones
+from pandas.compat._optional import import_optional_dependency
 from pandas.errors import PerformanceWarning
 
 from pandas.core.dtypes.common import (
@@ -448,11 +449,7 @@ def __init__(self, path, mode=None, complevel=None, complib=None,
         if 'format' in kwargs:
             raise ValueError('format is not a defined argument for HDFStore')
 
-        try:
-            import tables  # noqa
-        except ImportError as ex:  # pragma: no cover
-            raise ImportError('HDFStore requires PyTables, "{ex!s}" problem '
-                              'importing'.format(ex=ex))
+        tables = import_optional_dependency("tables")
 
         if complib is not None and complib not in tables.filters.all_complibs:
             raise ValueError(
diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py
--- a/pandas/util/_print_versions.py
+++ b/pandas/util/_print_versions.py
@@ -1,5 +1,4 @@
 import codecs
-import importlib
 import locale
 import os
 import platform
@@ -7,6 +6,9 @@
 import subprocess
 import sys
 
+from pandas.compat._optional import (
+    VERSIONS, _get_version, import_optional_dependency)
+
 
 def get_sys_info():
     "Returns system information as a dict"
@@ -58,60 +60,49 @@ def get_sys_info():
 
 def show_versions(as_json=False):
     sys_info = get_sys_info()
-
     deps = [
-        # (MODULE_NAME, f(mod) -> mod version)
-        ("pandas", lambda mod: mod.__version__),
-        ("pytest", lambda mod: mod.__version__),
-        ("pip", lambda mod: mod.__version__),
-        ("setuptools", lambda mod: mod.__version__),
-        ("Cython", lambda mod: mod.__version__),
-        ("numpy", lambda mod: mod.version.version),
-        ("scipy", lambda mod: mod.version.version),
-        ("pyarrow", lambda mod: mod.__version__),
-        ("xarray", lambda mod: mod.__version__),
-        ("IPython", lambda mod: mod.__version__),
-        ("sphinx", lambda mod: mod.__version__),
-        ("patsy", lambda mod: mod.__version__),
-        ("dateutil", lambda mod: mod.__version__),
-        ("pytz", lambda mod: mod.VERSION),
-        ("blosc", lambda mod: mod.__version__),
-        ("bottleneck", lambda mod: mod.__version__),
-        ("tables", lambda mod: mod.__version__),
-        ("numexpr", lambda mod: mod.__version__),
-        ("feather", lambda mod: mod.__version__),
-        ("matplotlib", lambda mod: mod.__version__),
-        ("openpyxl", lambda mod: mod.__version__),
-        ("xlrd", lambda mod: mod.__VERSION__),
-        ("xlwt", lambda mod: mod.__VERSION__),
-        ("xlsxwriter", lambda mod: mod.__version__),
-        ("lxml.etree", lambda mod: mod.__version__),
-        ("bs4", lambda mod: mod.__version__),
-        ("html5lib", lambda mod: mod.__version__),
-        ("sqlalchemy", lambda mod: mod.__version__),
-        ("pymysql", lambda mod: mod.__version__),
-        ("psycopg2", lambda mod: mod.__version__),
-        ("jinja2", lambda mod: mod.__version__),
-        ("s3fs", lambda mod: mod.__version__),
-        ("fastparquet", lambda mod: mod.__version__),
-        ("pandas_gbq", lambda mod: mod.__version__),
-        ("pandas_datareader", lambda mod: mod.__version__),
-        ("gcsfs", lambda mod: mod.__version__),
+        'pandas',
+        # required
+        'numpy',
+        'pytz',
+        'dateutil',
+        # install / build,
+        'pip',
+        'setuptools',
+        'Cython',
+        # test
+        'pytest',
+        'hypothesis',
+        # docs
+        "sphinx",
+        # Other, need a min version
+        "blosc",
+        "feather",
+        "xlsxwriter",
+        "lxml.etree",
+        "html5lib",
+        "pymysql",
+        "psycopg2",
+        "jinja2",
+        # Other, not imported.
+        "IPython",
+        "pandas_datareader",
     ]
 
-    deps_blob = list()
-    for (modname, ver_f) in deps:
-        try:
-            if modname in sys.modules:
-                mod = sys.modules[modname]
-            else:
-                mod = importlib.import_module(modname)
-            ver = ver_f(mod)
-            deps_blob.append((modname, ver))
-        except ImportError:
-            deps_blob.append((modname, None))
+    deps.extend(list(VERSIONS))
+    deps_blob = []
 
-    if (as_json):
+    for modname in deps:
+        mod = import_optional_dependency(modname,
+                                         raise_on_missing=False,
+                                         on_version="ignore")
+        if mod:
+            ver = _get_version(mod)
+        else:
+            ver = None
+        deps_blob.append((modname, ver))
+
+    if as_json:
         try:
             import json
         except ImportError:
@@ -126,16 +117,15 @@ def show_versions(as_json=False):
                 json.dump(j, f, indent=2)
 
     else:
-
+        maxlen = max(len(x) for x in deps)
+        tpl = '{{k:<{maxlen}}}: {{stat}}'.format(maxlen=maxlen)
         print("\nINSTALLED VERSIONS")
         print("------------------")
-
         for k, stat in sys_info:
-            print("{k}: {stat}".format(k=k, stat=stat))
-
+            print(tpl.format(k=k, stat=stat))
         print("")
         for k, stat in deps_blob:
-            print("{k}: {stat}".format(k=k, stat=stat))
+            print(tpl.format(k=k, stat=stat))
 
 
 def main():


Test patch:
diff --git a/pandas/tests/io/test_pytables_missing.py b/pandas/tests/io/test_pytables_missing.py
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/test_pytables_missing.py
@@ -0,0 +1,14 @@
+import pytest
+
+import pandas.util._test_decorators as td
+
+import pandas as pd
+import pandas.util.testing as tm
+
+
+@td.skip_if_installed("tables")
+def test_pytables_raises():
+    df = pd.DataFrame({"A": [1, 2]})
+    with pytest.raises(ImportError, match="tables"):
+        with tm.ensure_clean("foo.h5") as path:
+            df.to_hdf(path, "df")
diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py
--- a/pandas/util/_test_decorators.py
+++ b/pandas/util/_test_decorators.py
@@ -100,6 +100,23 @@ def _skip_if_no_scipy():
                 safe_import('scipy.signal'))
 
 
+def skip_if_installed(
+    package: str,
+) -> MarkDecorator:
+    """
+    Skip a test if a package is installed.
+
+    Parameters
+    ----------
+    package : str
+        The name of the package.
+    """
+    return pytest.mark.skipif(
+        safe_import(package),
+        reason="Skipping because {} is installed.".format(package)
+    )
+
+
 def skip_if_no(
     package: str,
     min_version: Optional[str] = None