From 10d8f8745b8f4eee1bb035ba2114bb1fade24006 Mon Sep 17 00:00:00 2001 From: Thomas Li Date: Sat, 2 Jan 2021 14:40:08 -0800 Subject: [PATCH 1/2] Add support to import optional submodule and add tests --- pandas/compat/_optional.py | 31 +++++++++++++++++------- pandas/tests/test_optional_dependency.py | 29 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index 3775a47d44521..eb678e9ac7832 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! @@ -11,7 +13,7 @@ "fsspec": "0.7.4", "fastparquet": "0.3.2", "gcsfs": "0.6.0", - "lxml.etree": "4.3.0", + "lxml": "4.3.0", "matplotlib": "2.2.3", "numexpr": "2.6.8", "odfpy": "1.3.0", @@ -38,7 +40,6 @@ INSTALL_MAPPING = { "bs4": "beautifulsoup4", "bottleneck": "Bottleneck", - "lxml.etree": "lxml", "odf": "odfpy", "pandas_gbq": "pandas-gbq", "sqlalchemy": "SQLAlchemy", @@ -58,7 +59,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 +75,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 +89,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 : Optional[str] + Specify a minimum version that is different from the global pandas + minimum version required. Returns ------- maybe_module : Optional[ModuleType] @@ -110,13 +116,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..7d54267a44fb7 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): + print(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) From a4ea26bc587b6ab0192ba6e7ff85598879dc4438 Mon Sep 17 00:00:00 2001 From: Thomas Li Date: Sun, 3 Jan 2021 11:38:02 -0800 Subject: [PATCH 2/2] Cleanup mistakes and address code review --- pandas/compat/_optional.py | 5 +++-- pandas/tests/test_optional_dependency.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py index eb678e9ac7832..def881b8fd863 100644 --- a/pandas/compat/_optional.py +++ b/pandas/compat/_optional.py @@ -13,7 +13,7 @@ "fsspec": "0.7.4", "fastparquet": "0.3.2", "gcsfs": "0.6.0", - "lxml": "4.3.0", + "lxml.etree": "4.3.0", "matplotlib": "2.2.3", "numexpr": "2.6.8", "odfpy": "1.3.0", @@ -40,6 +40,7 @@ INSTALL_MAPPING = { "bs4": "beautifulsoup4", "bottleneck": "Bottleneck", + "lxml.etree": "lxml", "odf": "odfpy", "pandas_gbq": "pandas-gbq", "sqlalchemy": "SQLAlchemy", @@ -89,7 +90,7 @@ 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 : Optional[str] + min_version : str, default None Specify a minimum version that is different from the global pandas minimum version required. Returns diff --git a/pandas/tests/test_optional_dependency.py b/pandas/tests/test_optional_dependency.py index 7d54267a44fb7..304ec124ac8c5 100644 --- a/pandas/tests/test_optional_dependency.py +++ b/pandas/tests/test_optional_dependency.py @@ -60,7 +60,7 @@ def test_submodule(monkeypatch): match = "Pandas requires .*1.0.0.* of .fakemodule.*'0.9.0'" with pytest.raises(ImportError, match=match): - print(import_optional_dependency("fakemodule.submodule")) + import_optional_dependency("fakemodule.submodule") with tm.assert_produces_warning(UserWarning): result = import_optional_dependency("fakemodule.submodule", on_version="warn")