diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 3775a47d44521..def881b8fd863 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -1,6 +1,8 @@ import distutils.version import importlib +import sys import types +from typing import Optional import warnings # Update install.rst when updating versions! @@ -58,7 +60,11 @@ def _get_version(module: types.ModuleType) -> str: def import_optional_dependency( - name: str, extra: str = "", raise_on_missing: bool = True, on_version: str = "raise" + name: str, + extra: str = "", + raise_on_missing: bool = True, + on_version: str = "raise", + min_version: Optional[str] = None, ): """ Import an optional dependency. @@ -70,8 +76,7 @@ def import_optional_dependency( Parameters ---------- name : str - The module name. This should be top-level only, so that the - version may be checked. + The module name. extra : str Additional text to include in the ImportError message. raise_on_missing : bool, default True @@ -85,7 +90,9 @@ def import_optional_dependency( * ignore: Return the module, even if the version is too old. It's expected that users validate the version locally when using ``on_version="ignore"`` (see. ``io/html.py``) - + min_version : str, default None + Specify a minimum version that is different from the global pandas + minimum version required. Returns ------- maybe_module : Optional[ModuleType] @@ -110,13 +117,20 @@ def import_optional_dependency( else: return None - minimum_version = VERSIONS.get(name) + # Handle submodules: if we have submodule, grab parent module from sys.modules + parent = name.split(".")[0] + if parent != name: + install_name = parent + module_to_get = sys.modules[install_name] + else: + module_to_get = module + minimum_version = min_version if min_version is not None else VERSIONS.get(parent) if minimum_version: - version = _get_version(module) + version = _get_version(module_to_get) if distutils.version.LooseVersion(version) < minimum_version: assert on_version in {"warn", "raise", "ignore"} msg = ( - f"Pandas requires version '{minimum_version}' or newer of '{name}' " + f"Pandas requires version '{minimum_version}' or newer of '{parent}' " f"(version '{version}' currently installed)." ) if on_version == "warn": diff --git a/pandas/tests/test_optional_dependency.py b/pandas/tests/test_optional_dependency.py index e5ed69b7703b1..304ec124ac8c5 100644 --- a/pandas/tests/test_optional_dependency.py +++ b/pandas/tests/test_optional_dependency.py @@ -33,6 +33,10 @@ def test_bad_version(monkeypatch): with pytest.raises(ImportError, match=match): import_optional_dependency("fakemodule") + # Test min_version parameter + result = import_optional_dependency("fakemodule", min_version="0.8") + assert result is module + with tm.assert_produces_warning(UserWarning): result = import_optional_dependency("fakemodule", on_version="warn") assert result is None @@ -42,6 +46,31 @@ def test_bad_version(monkeypatch): assert result is module +def test_submodule(monkeypatch): + # Create a fake module with a submodule + name = "fakemodule" + module = types.ModuleType(name) + module.__version__ = "0.9.0" + sys.modules[name] = module + sub_name = "submodule" + submodule = types.ModuleType(sub_name) + setattr(module, sub_name, submodule) + sys.modules[f"{name}.{sub_name}"] = submodule + monkeypatch.setitem(VERSIONS, name, "1.0.0") + + match = "Pandas requires .*1.0.0.* of .fakemodule.*'0.9.0'" + with pytest.raises(ImportError, match=match): + import_optional_dependency("fakemodule.submodule") + + with tm.assert_produces_warning(UserWarning): + result = import_optional_dependency("fakemodule.submodule", on_version="warn") + assert result is None + + module.__version__ = "1.0.0" # exact match is OK + result = import_optional_dependency("fakemodule.submodule") + assert result is submodule + + def test_no_version_raises(monkeypatch): name = "fakemodule" module = types.ModuleType(name)