Skip to content

Commit 9952626

Browse files
authored
ENH: Add support to import optional submodule and specify different min_version than default (#38925)
1 parent 17b12c2 commit 9952626

File tree

2 files changed

+50
-7
lines changed

2 files changed

+50
-7
lines changed

pandas/compat/_optional.py

+21-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import distutils.version
22
import importlib
3+
import sys
34
import types
5+
from typing import Optional
46
import warnings
57

68
# Update install.rst when updating versions!
@@ -58,7 +60,11 @@ def _get_version(module: types.ModuleType) -> str:
5860

5961

6062
def import_optional_dependency(
61-
name: str, extra: str = "", raise_on_missing: bool = True, on_version: str = "raise"
63+
name: str,
64+
extra: str = "",
65+
raise_on_missing: bool = True,
66+
on_version: str = "raise",
67+
min_version: Optional[str] = None,
6268
):
6369
"""
6470
Import an optional dependency.
@@ -70,8 +76,7 @@ def import_optional_dependency(
7076
Parameters
7177
----------
7278
name : str
73-
The module name. This should be top-level only, so that the
74-
version may be checked.
79+
The module name.
7580
extra : str
7681
Additional text to include in the ImportError message.
7782
raise_on_missing : bool, default True
@@ -85,7 +90,9 @@ def import_optional_dependency(
8590
* ignore: Return the module, even if the version is too old.
8691
It's expected that users validate the version locally when
8792
using ``on_version="ignore"`` (see. ``io/html.py``)
88-
93+
min_version : str, default None
94+
Specify a minimum version that is different from the global pandas
95+
minimum version required.
8996
Returns
9097
-------
9198
maybe_module : Optional[ModuleType]
@@ -110,13 +117,20 @@ def import_optional_dependency(
110117
else:
111118
return None
112119

113-
minimum_version = VERSIONS.get(name)
120+
# Handle submodules: if we have submodule, grab parent module from sys.modules
121+
parent = name.split(".")[0]
122+
if parent != name:
123+
install_name = parent
124+
module_to_get = sys.modules[install_name]
125+
else:
126+
module_to_get = module
127+
minimum_version = min_version if min_version is not None else VERSIONS.get(parent)
114128
if minimum_version:
115-
version = _get_version(module)
129+
version = _get_version(module_to_get)
116130
if distutils.version.LooseVersion(version) < minimum_version:
117131
assert on_version in {"warn", "raise", "ignore"}
118132
msg = (
119-
f"Pandas requires version '{minimum_version}' or newer of '{name}' "
133+
f"Pandas requires version '{minimum_version}' or newer of '{parent}' "
120134
f"(version '{version}' currently installed)."
121135
)
122136
if on_version == "warn":

pandas/tests/test_optional_dependency.py

+29
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def test_bad_version(monkeypatch):
3333
with pytest.raises(ImportError, match=match):
3434
import_optional_dependency("fakemodule")
3535

36+
# Test min_version parameter
37+
result = import_optional_dependency("fakemodule", min_version="0.8")
38+
assert result is module
39+
3640
with tm.assert_produces_warning(UserWarning):
3741
result = import_optional_dependency("fakemodule", on_version="warn")
3842
assert result is None
@@ -42,6 +46,31 @@ def test_bad_version(monkeypatch):
4246
assert result is module
4347

4448

49+
def test_submodule(monkeypatch):
50+
# Create a fake module with a submodule
51+
name = "fakemodule"
52+
module = types.ModuleType(name)
53+
module.__version__ = "0.9.0"
54+
sys.modules[name] = module
55+
sub_name = "submodule"
56+
submodule = types.ModuleType(sub_name)
57+
setattr(module, sub_name, submodule)
58+
sys.modules[f"{name}.{sub_name}"] = submodule
59+
monkeypatch.setitem(VERSIONS, name, "1.0.0")
60+
61+
match = "Pandas requires .*1.0.0.* of .fakemodule.*'0.9.0'"
62+
with pytest.raises(ImportError, match=match):
63+
import_optional_dependency("fakemodule.submodule")
64+
65+
with tm.assert_produces_warning(UserWarning):
66+
result = import_optional_dependency("fakemodule.submodule", on_version="warn")
67+
assert result is None
68+
69+
module.__version__ = "1.0.0" # exact match is OK
70+
result = import_optional_dependency("fakemodule.submodule")
71+
assert result is submodule
72+
73+
4574
def test_no_version_raises(monkeypatch):
4675
name = "fakemodule"
4776
module = types.ModuleType(name)

0 commit comments

Comments
 (0)