Source code for python_utils.import_

"""
This module provides utilities for importing modules and handling exceptions.

Classes:
    DummyError(Exception):
        A custom exception class used as a default for exception handling.

Functions:
    import_global(name, modules=None, exceptions=DummyError, locals_=None,
        globals_=None, level=-1):
        Imports the requested items into the global scope, with support for
        relative imports and custom exception handling.
"""

import importlib
import types
import typing

from python_utils import _aliases


[docs] class DummyError(Exception): """A custom exception class used as a default for exception handling."""
#: Backwards-compatible legacy alias for ``DummyError``. DummyException = DummyError def _get_attribute(module: typing.Any, attr: str, name: str) -> typing.Any: """Return ``module.attr``, importing it as a submodule when needed. A submodule only becomes an attribute of its parent once something has imported it. Args: module: The module, or other object, to take the attribute from. attr: The name of the attribute or submodule. name: The full dotted name that is being imported, for the error. Returns: The attribute or the imported submodule. Raises: ImportError: When ``module`` has no such attribute or submodule. An error that the submodule raises while it is imported is passed on as it is, a missing dependency included. """ try: return getattr(module, attr) except AttributeError as error: if not isinstance(module, types.ModuleType): # The same error as for a missing module, as it always was. raise ImportError( # noqa: TRY004 f'No module named {name}' ) from error submodule: str = f'{module.__name__}.{attr}' try: return importlib.import_module(submodule) except ModuleNotFoundError as error: missing: str = error.name or '' if missing != submodule and not submodule.startswith(f'{missing}.'): # The submodule exists and one of its own imports is missing. raise raise ImportError(f'No module named {name}') from error
[docs] def import_global( # noqa: C901 name: str, modules: list[str] | None = None, exceptions: _aliases.ExceptionsType = DummyError, locals_: _aliases.OptionalScope = None, globals_: _aliases.OptionalScope = None, level: int = -1, ) -> typing.Any: # sourcery skip: hoist-if-from-if """Import the requested items into the global scope. WARNING! this method _will_ overwrite your global scope If you have a variable named `path` and you call `import_global('sys')` it will be overwritten with `sys.path` Args: name (str): the name of the module to import, e.g. sys modules (list[str]): the names to import from the module, use None for everything. An empty list also imports everything. A single name needs a list as well, a bare string is read as a collection of one-character names. Names that start with an underscore are never imported. exceptions (Exception): the exception to catch, e.g. ImportError locals_: the `locals()` method (in case you need a different scope) globals_: the `globals()` method (in case you need a different scope) level (int): the level to import from, this can be used for relative imports """ frame = None name_parts: list[str] = name.split('.') modules_set: set[str] = set() try: # If locals_ or globals_ are not given, autodetect them by inspecting # the current stack if locals_ is None or globals_ is None: import inspect frame = inspect.stack()[1][0] if locals_ is None: locals_ = frame.f_locals if globals_ is None: globals_ = frame.f_globals try: # Relative imports are supported (from .spam import eggs) if not name_parts[0]: name_parts = name_parts[1:] level = 1 # raise IOError((name, level)) module = __import__( name=name_parts[0] or '.', globals=globals_, locals=locals_, fromlist=name_parts[1:], level=max(level, 0), ) # Make sure we get the right part of a dotted import (i.e. # spam.eggs should return eggs, not spam) for attr in name_parts[1:]: module = _get_attribute(module, attr, '.'.join(name_parts)) # If no list of modules is given, autodetect from either __all__ # or a dir() of the module if not modules: modules_set = set(getattr(module, '__all__', dir(module))) else: modules_set = set(modules).intersection(dir(module)) # Add all items in modules to the global scope for k in set(dir(module)).intersection(modules_set): if k and k[0] != '_': globals_[k] = getattr(module, k) except exceptions as e: return e finally: # Clean up, just to be sure del ( # pyrefly: ignore[unsupported-delete] name, name_parts, modules, modules_set, exceptions, locals_, globals_, frame, )