Last active
September 4, 2024 00:04
-
-
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...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As to
v1_deferred_imports()
, the culprit for the immediate loading of parent modules seems to beimportlib.util.find_spec()
:If wonder if we could, by any chance, replace this with a direct instantiation of
importlib.machinery.ModuleSpec
or something to bypass this limitation.