From a7f66864e86aee5bed7bf5ce1a5ba98b81e80598 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 17 Mar 2026 16:12:31 -0400 Subject: [PATCH 1/3] Add option to attach for lazy submodules Signed-off-by: Samuel Monson --- src/lazy_loader/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/lazy_loader/__init__.py b/src/lazy_loader/__init__.py index 23b5382..ce2f87e 100644 --- a/src/lazy_loader/__init__.py +++ b/src/lazy_loader/__init__.py @@ -13,6 +13,7 @@ import threading import types import warnings +from functools import partial __version__ = "0.6rc0.dev0" __all__ = ["attach", "attach_stub", "load"] @@ -21,7 +22,12 @@ threadlock = threading.Lock() -def attach(package_name, submodules=None, submod_attrs=None): +def attach( + package_name, + submodules=None, + submod_attrs=None, + lazy_submodules=False, +): """Attach lazily loaded submodules, functions, or other attributes. Typically, modules import submodules and attributes as follows:: @@ -50,6 +56,9 @@ def attach(package_name, submodules=None, submod_attrs=None): submod_attrs : dict Dictionary of submodule -> list of attributes / functions. These attributes are imported as they are used. + lazy_submodules : bool + Whether to lazily load submodules. If set to `True`, submodules are + returned as lazy proxies. Returns ------- @@ -64,6 +73,11 @@ def attach(package_name, submodules=None, submod_attrs=None): else: submodules = set(submodules) + if lazy_submodules: + import_func = partial(load, suppress_warning=True) + else: + import_func = importlib.import_module + attr_to_modules = { attr: mod for mod, attrs in submod_attrs.items() for attr in attrs } @@ -72,10 +86,10 @@ def attach(package_name, submodules=None, submod_attrs=None): def __getattr__(name): if name in submodules: - return importlib.import_module(f"{package_name}.{name}") + return import_func(f"{package_name}.{name}") elif name in attr_to_modules: submod_path = f"{package_name}.{attr_to_modules[name]}" - submod = importlib.import_module(submod_path) + submod = import_func(submod_path) attr = getattr(submod, name) # If the attribute lives in a file (module) with the same From 6a00f4d4ecd0e271d5e7b7da5e08d3cefa125f27 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 17 Mar 2026 17:25:02 -0400 Subject: [PATCH 2/3] Add test cov for lazy load attach Signed-off-by: Samuel Monson --- tests/test_lazy_loader.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_lazy_loader.py b/tests/test_lazy_loader.py index d68537f..419b1b0 100644 --- a/tests/test_lazy_loader.py +++ b/tests/test_lazy_loader.py @@ -86,7 +86,8 @@ def test_lazy_import_nonbuiltins(): sp.pi -def test_lazy_attach(): +@pytest.mark.parametrize("lazy_submodules", [False, True]) +def test_lazy_attach(lazy_submodules): name = "mymod" submods = ["mysubmodule", "anothersubmodule"] myall = {"not_real_submod": ["some_var_or_func"]} @@ -96,8 +97,12 @@ def test_lazy_attach(): "name": name, "submods": submods, "myall": myall, + "lazy_submods": lazy_submodules, } - s = "__getattr__, __lazy_dir__, __all__ = attach(name, submods, myall)" + s = ( + "__getattr__, __lazy_dir__, __all__ = " + "attach(name, submods, myall, lazy_submodules=lazy_submods)" + ) exec(s, {}, locls) expected = { @@ -105,6 +110,7 @@ def test_lazy_attach(): "name": name, "submods": submods, "myall": myall, + "lazy_submods": lazy_submodules, "__getattr__": None, "__lazy_dir__": None, "__all__": None, @@ -135,9 +141,13 @@ def test_lazy_attach_noattrs(): assert all_ == sorted(submods) -def test_lazy_attach_returns_copies(): +@pytest.mark.parametrize("lazy_submodules", [False, True]) +def test_lazy_attach_returns_copies(lazy_submodules): _get, _dir, _all = lazy.attach( - __name__, ["my_submodule", "another_submodule"], {"foo": ["some_attr"]} + __name__, + ["my_submodule", "another_submodule"], + {"foo": ["some_attr"]}, + lazy_submodules=lazy_submodules, ) assert _dir() is not _dir() assert _dir() == _all From 75cd72137de9dff1c358adf86cafd8862ce9a5a4 Mon Sep 17 00:00:00 2001 From: Samuel Monson Date: Tue, 17 Mar 2026 17:37:45 -0400 Subject: [PATCH 3/3] Drop getattr from lazy load Signed-off-by: Samuel Monson --- src/lazy_loader/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/lazy_loader/__init__.py b/src/lazy_loader/__init__.py index ce2f87e..8448ee6 100644 --- a/src/lazy_loader/__init__.py +++ b/src/lazy_loader/__init__.py @@ -13,7 +13,6 @@ import threading import types import warnings -from functools import partial __version__ = "0.6rc0.dev0" __all__ = ["attach", "attach_stub", "load"] @@ -58,7 +57,8 @@ def attach( These attributes are imported as they are used. lazy_submodules : bool Whether to lazily load submodules. If set to `True`, submodules are - returned as lazy proxies. + returned as lazy proxies. Note that attribute access from + submod_attrs will trigger the import of the submodule. Returns ------- @@ -73,11 +73,6 @@ def attach( else: submodules = set(submodules) - if lazy_submodules: - import_func = partial(load, suppress_warning=True) - else: - import_func = importlib.import_module - attr_to_modules = { attr: mod for mod, attrs in submod_attrs.items() for attr in attrs } @@ -86,10 +81,14 @@ def attach( def __getattr__(name): if name in submodules: - return import_func(f"{package_name}.{name}") + submod_path = f"{package_name}.{name}" + if lazy_submodules: + return load(submod_path, suppress_warning=True) + else: + return importlib.import_module(submod_path) elif name in attr_to_modules: submod_path = f"{package_name}.{attr_to_modules[name]}" - submod = import_func(submod_path) + submod = importlib.import_module(submod_path) attr = getattr(submod, name) # If the attribute lives in a file (module) with the same