Skip to content

ENH: Add support to import optional submodule and specify different min_version than default #38925

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
Jan 4, 2021
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
28 changes: 21 additions & 7 deletions pandas/compat/_optional.py
Original file line number Diff line number Diff line change
@@ -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!
Expand Down Expand Up @@ -58,7 +60,11 @@ def _get_version(module: types.ModuleType) -> str:


def import_optional_dependency(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe we could / should min_version in the codebase, but can be a followon

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.
Expand All @@ -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
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you create an issue (and PR!) to consolidate raise_on_missing / on_version which really should just be errors='raise|warn|ignore'

Specify a minimum version that is different from the global pandas
minimum version required.
Returns
-------
maybe_module : Optional[ModuleType]
Expand All @@ -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":
Expand Down
29 changes: 29 additions & 0 deletions pandas/tests/test_optional_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down