Skip to content

Commit 2663e31

Browse files
committed
feat: add generic try_import(module_name, load=True) helper
1 parent ea77463 commit 2663e31

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

lumen/tests/sources/test_optional_imports.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import pytest
1616

17-
from lumen.util import try_import_xarray
17+
from lumen.util import try_import, try_import_xarray
1818

1919
# Each entry: (module_path, class_name, guard_package, pip_extra)
2020
OPTIONAL_SOURCES = [
@@ -88,6 +88,29 @@ def mock_import(name, *args, **kwargs):
8888
sys.modules.update(saved)
8989

9090

91+
def test_try_import_returns_module_when_installed():
92+
"""try_import returns the module object for an importable module."""
93+
import pandas
94+
assert try_import("pandas") is pandas
95+
96+
97+
def test_try_import_returns_none_when_missing():
98+
"""try_import returns None (not raises) for a module that cannot be imported."""
99+
assert try_import("a_module_that_does_not_exist_xyz") is None
100+
101+
102+
def test_try_import_load_false_returns_already_imported_module():
103+
"""try_import(load=False) returns a module that is already imported."""
104+
import json
105+
assert try_import("json", load=False) is json
106+
107+
108+
def test_try_import_load_false_never_imports():
109+
"""try_import(load=False) must not trigger an import, only consult sys.modules."""
110+
with patch("lumen.util.importlib.import_module", side_effect=AssertionError("imported")):
111+
assert try_import("a_module_that_does_not_exist_xyz", load=False) is None
112+
113+
91114
def test_try_import_xarray_returns_module_when_installed():
92115
"""try_import_xarray returns the xarray module when xarray and xarray-sql are installed."""
93116
xr = pytest.importorskip("xarray")

lumen/util.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,22 @@ def normalize_table_name(name: str) -> str:
493493
return re.sub(r'\W+', '_', name).strip('_').lower()
494494

495495

496+
def try_import(module_name, load=True):
497+
"""Import and return a module, or None if it is unavailable.
498+
499+
With load=False the module is returned only if it has already been
500+
imported, a cheap check that never triggers an import; this lets a hot path
501+
ask whether an optional dependency is in play without paying to import it.
502+
With load=True (the default) the module is imported on demand.
503+
"""
504+
if not load:
505+
return sys.modules.get(module_name)
506+
try:
507+
return importlib.import_module(module_name)
508+
except ImportError:
509+
return None
510+
511+
496512
def try_import_xarray():
497513
"""Import and return xarray, or None if xarray or xarray-sql is unavailable."""
498514
try:

0 commit comments

Comments
 (0)