CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
__init__.py350 linesDownload Raw Back to lazy_loader
1"""2lazy_loader3===========4 5Makes it easy to load subpackages and functions on demand.6"""7 8import ast9import importlib10import importlib.util11import os12import sys13import threading14import types15import warnings16 17__version__ = "0.4"18__all__ = ["attach", "load", "attach_stub"]19 20 21threadlock = threading.Lock()22 23 24def attach(package_name, submodules=None, submod_attrs=None):25    """Attach lazily loaded submodules, functions, or other attributes.26 27    Typically, modules import submodules and attributes as follows::28 29      import mysubmodule30      import anothersubmodule31 32      from .foo import someattr33 34    The idea is to replace a package's `__getattr__`, `__dir__`, and35    `__all__`, such that all imports work exactly the way they would36    with normal imports, except that the import occurs upon first use.37 38    The typical way to call this function, replacing the above imports, is::39 40      __getattr__, __dir__, __all__ = lazy.attach(41        __name__,42        ['mysubmodule', 'anothersubmodule'],43        {'foo': ['someattr']}44      )45 46    This functionality requires Python 3.7 or higher.47 48    Parameters49    ----------50    package_name : str51        Typically use ``__name__``.52    submodules : set53        List of submodules to attach.54    submod_attrs : dict55        Dictionary of submodule -> list of attributes / functions.56        These attributes are imported as they are used.57 58    Returns59    -------60    __getattr__, __dir__, __all__61 62    """63    if submod_attrs is None:64        submod_attrs = {}65 66    if submodules is None:67        submodules = set()68    else:69        submodules = set(submodules)70 71    attr_to_modules = {72        attr: mod for mod, attrs in submod_attrs.items() for attr in attrs73    }74 75    __all__ = sorted(submodules | attr_to_modules.keys())76 77    def __getattr__(name):78        if name in submodules:79            return importlib.import_module(f"{package_name}.{name}")80        elif name in attr_to_modules:81            submod_path = f"{package_name}.{attr_to_modules[name]}"82            submod = importlib.import_module(submod_path)83            attr = getattr(submod, name)84 85            # If the attribute lives in a file (module) with the same86            # name as the attribute, ensure that the attribute and *not*87            # the module is accessible on the package.88            if name == attr_to_modules[name]:89                pkg = sys.modules[package_name]90                pkg.__dict__[name] = attr91 92            return attr93        else:94            raise AttributeError(f"No {package_name} attribute {name}")95 96    def __dir__():97        return __all__98 99    if os.environ.get("EAGER_IMPORT", ""):100        for attr in set(attr_to_modules.keys()) | submodules:101            __getattr__(attr)102 103    return __getattr__, __dir__, list(__all__)104 105 106class DelayedImportErrorModule(types.ModuleType):107    def __init__(self, frame_data, *args, message, **kwargs):108        self.__frame_data = frame_data109        self.__message = message110        super().__init__(*args, **kwargs)111 112    def __getattr__(self, x):113        if x in ("__class__", "__file__", "__frame_data", "__message"):114            super().__getattr__(x)115        else:116            fd = self.__frame_data117            raise ModuleNotFoundError(118                f"{self.__message}\n\n"119                "This error is lazily reported, having originally occured in\n"120                f'  File {fd["filename"]}, line {fd["lineno"]}, in {fd["function"]}\n\n'121                f'----> {"".join(fd["code_context"] or "").strip()}'122            )123 124 125def load(fullname, *, require=None, error_on_import=False):126    """Return a lazily imported proxy for a module.127 128    We often see the following pattern::129 130      def myfunc():131          import numpy as np132          np.norm(...)133          ....134 135    Putting the import inside the function prevents, in this case,136    `numpy`, from being imported at function definition time.137    That saves time if `myfunc` ends up not being called.138 139    This `load` function returns a proxy module that, upon access, imports140    the actual module.  So the idiom equivalent to the above example is::141 142      np = lazy.load("numpy")143 144      def myfunc():145          np.norm(...)146          ....147 148    The initial import time is fast because the actual import is delayed149    until the first attribute is requested. The overall import time may150    decrease as well for users that don't make use of large portions151    of your library.152 153    Warning154    -------155    While lazily loading *sub*packages technically works, it causes the156    package (that contains the subpackage) to be eagerly loaded even157    if the package is already lazily loaded.158    So, you probably shouldn't use subpackages with this `load` feature.159    Instead you should encourage the package maintainers to use the160    `lazy_loader.attach` to make their subpackages load lazily.161 162    Parameters163    ----------164    fullname : str165        The full name of the module or submodule to import.  For example::166 167          sp = lazy.load('scipy')  # import scipy as sp168 169    require : str170        A dependency requirement as defined in PEP-508.  For example::171 172          "numpy >=1.24"173 174        If defined, the proxy module will raise an error if the installed175        version does not satisfy the requirement.176 177    error_on_import : bool178        Whether to postpone raising import errors until the module is accessed.179        If set to `True`, import errors are raised as soon as `load` is called.180 181    Returns182    -------183    pm : importlib.util._LazyModule184        Proxy module.  Can be used like any regularly imported module.185        Actual loading of the module occurs upon first attribute request.186 187    """188    with threadlock:189        module = sys.modules.get(fullname)190        have_module = module is not None191 192        # Most common, short-circuit193        if have_module and require is None:194            return module195 196        if "." in fullname:197            msg = (198                "subpackages can technically be lazily loaded, but it causes the "199                "package to be eagerly loaded even if it is already lazily loaded."200                "So, you probably shouldn't use subpackages with this lazy feature."201            )202            warnings.warn(msg, RuntimeWarning)203 204        spec = None205 206        if not have_module:207            spec = importlib.util.find_spec(fullname)208            have_module = spec is not None209 210        if not have_module:211            not_found_message = f"No module named '{fullname}'"212        elif require is not None:213            try:214                have_module = _check_requirement(require)215            except ModuleNotFoundError as e:216                raise ValueError(217                    f"Found module '{fullname}' but cannot test "218                    "requirement '{require}'. "219                    "Requirements must match distribution name, not module name."220                ) from e221 222            not_found_message = f"No distribution can be found matching '{require}'"223 224        if not have_module:225            if error_on_import:226                raise ModuleNotFoundError(not_found_message)227            import inspect228 229            try:230                parent = inspect.stack()[1]231                frame_data = {232                    "filename": parent.filename,233                    "lineno": parent.lineno,234                    "function": parent.function,235                    "code_context": parent.code_context,236                }237                return DelayedImportErrorModule(238                    frame_data,239                    "DelayedImportErrorModule",240                    message=not_found_message,241                )242            finally:243                del parent244 245        if spec is not None:246            module = importlib.util.module_from_spec(spec)247            sys.modules[fullname] = module248 249            loader = importlib.util.LazyLoader(spec.loader)250            loader.exec_module(module)251 252    return module253 254 255def _check_requirement(require: str) -> bool:256    """Verify that a package requirement is satisfied257 258    If the package is required, a ``ModuleNotFoundError`` is raised259    by ``importlib.metadata``.260 261    Parameters262    ----------263    require : str264        A dependency requirement as defined in PEP-508265 266    Returns267    -------268    satisfied : bool269        True if the installed version of the dependency matches270        the specified version, False otherwise.271    """272    import packaging.requirements273 274    try:275        import importlib.metadata as importlib_metadata276    except ImportError:  # PY37277        import importlib_metadata278 279    req = packaging.requirements.Requirement(require)280    return req.specifier.contains(281        importlib_metadata.version(req.name),282        prereleases=True,283    )284 285 286class _StubVisitor(ast.NodeVisitor):287    """AST visitor to parse a stub file for submodules and submod_attrs."""288 289    def __init__(self):290        self._submodules = set()291        self._submod_attrs = {}292 293    def visit_ImportFrom(self, node: ast.ImportFrom):294        if node.level != 1:295            raise ValueError(296                "Only within-module imports are supported (`from .* import`)"297            )298        if node.module:299            attrs: list = self._submod_attrs.setdefault(node.module, [])300            aliases = [alias.name for alias in node.names]301            if "*" in aliases:302                raise ValueError(303                    "lazy stub loader does not support star import "304                    f"`from {node.module} import *`"305                )306            attrs.extend(aliases)307        else:308            self._submodules.update(alias.name for alias in node.names)309 310 311def attach_stub(package_name: str, filename: str):312    """Attach lazily loaded submodules, functions from a type stub.313 314    This is a variant on ``attach`` that will parse a `.pyi` stub file to315    infer ``submodules`` and ``submod_attrs``. This allows static type checkers316    to find imports, while still providing lazy loading at runtime.317 318    Parameters319    ----------320    package_name : str321        Typically use ``__name__``.322    filename : str323        Path to `.py` file which has an adjacent `.pyi` file.324        Typically use ``__file__``.325 326    Returns327    -------328    __getattr__, __dir__, __all__329        The same output as ``attach``.330 331    Raises332    ------333    ValueError334        If a stub file is not found for `filename`, or if the stubfile is formmated335        incorrectly (e.g. if it contains an relative import from outside of the module)336    """337    stubfile = (338        filename if filename.endswith("i") else f"{os.path.splitext(filename)[0]}.pyi"339    )340 341    if not os.path.exists(stubfile):342        raise ValueError(f"Cannot load imports from non-existent stub {stubfile!r}")343 344    with open(stubfile) as f:345        stub_node = ast.parse(f.read())346 347    visitor = _StubVisitor()348    visitor.visit(stub_node)349    return attach(package_name, visitor._submodules, visitor._submod_attrs)350