Skip to content

Instantly share code, notes, and snippets.

@mara004
Last active September 4, 2024 00:04
Show Gist options
  • Save mara004/6915e904797916b961e9c53b4fc874ec to your computer and use it in GitHub Desktop.
Save mara004/6915e904797916b961e9c53b4fc874ec to your computer and use it in GitHub Desktop.
Various attempts at deferred ("lazy") imports. None of these seems particularly satisfying, though. Missing PEP 690...
# SPDX-FileCopyrightText: 2024 geisserml <geisserml@gmail.com>
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause OR MPL-2.0
import sys
import importlib.util
def v1_deferred_import(modpath):
# FIXME If modpath points to a submodule (e.g. PIL.Image), the parent module will be loaded immediately when this function is called. What's more, non-deferred imports of the submodule will break. This seems to be a nasty limitation of the importlib APIs used here.
module = sys.modules.get(modpath, None)
if module is not None:
return module # shortcut
# assuming an optional dependency
# returning None will simply let it fail with an AttributeError when attempting to access the module
try:
spec = importlib.util.find_spec(modpath)
except ModuleNotFoundError:
return None
if spec is None:
return None
# see https://docs.python.org/3/library/importlib.html#implementing-lazy-imports
loader = importlib.util.LazyLoader(spec.loader)
spec.loader = loader
module = importlib.util.module_from_spec(spec)
sys.modules[modpath] = module
loader.exec_module(module)
return module
# ---------------
import importlib
import functools
if sys.version_info < (3, 8):
# NOTE This is not as good as a real cached property.
# https://github.com/penguinolog/backports.cached_property might be better.
def cached_property(func):
return property( functools.lru_cache(maxsize=1)(func) )
else:
cached_property = functools.cached_property
class v2_DeferredModule:
# NOTE Attribute assigment will affect only the wrapper, not the actual module.
def __init__(self, modpath):
self._modpath = modpath
def __repr__(self):
return f"<deferred module wrapper {self._modpath!r}>"
@cached_property
def _module(self):
# print("actually importing module...")
return importlib.import_module(self._modpath)
def __getattr__(self, k):
return getattr(self._module, k)
# ---------------
import importlib
class v3_ModulePlaceholder:
# NOTE Instances of this class are bound to a single namespace, so `from nsp import MyModule` would cause breakage. However, `nsp.MyModule` should work.
def __init__(self, modpath, nsp): # pass nsp=globals()
self._modpath = modpath
self._nsp = nsp
self._count = 0
def __repr__(self):
return f"<deferred module placeholder {self._modpath!r}>"
def __getattr__(self, k):
assert self._count == 0, "Placeholder must be replaced by actual module on first attribute access."
self._count += 1
name = self._modpath.replace(".", "_")
module = importlib.import_module(self._modpath)
self._nsp[name] = module
return getattr(module, k)
# ---------------
import importlib
import lazy_object_proxy # third-party
def v4_deferred_import(modpath):
return lazy_object_proxy.Proxy(lambda: importlib.import_module(modpath))
@mara004
Copy link
Author

mara004 commented Jul 13, 2024

As to v1_deferred_imports(), the culprit for the immediate loading of parent modules seems to be importlib.util.find_spec():

If name is for a submodule (contains a dot), the parent module is automatically imported.

If wonder if we could, by any chance, replace this with a direct instantiation of importlib.machinery.ModuleSpec or something to bypass this limitation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment