Source code for nanoutils._lazy_import

"""A private module for containg the :class:`LazyImporter` and :class:`MutableLazyImporter` classes.

Notes
-----
:class:`~nanoutils.LazyImporter` and :class:`~nanoutils.MutableLazyImporter`
should be imported from either :mod:`nanoutils` or :mod:`nanoutils.utils`.

"""  # noqa: E501

from __future__ import annotations

import sys
import types
import reprlib
import importlib
from collections.abc import Mapping, Callable, Iterable
from typing import Any, TypeVar, Type, Generic, TYPE_CHECKING

_T = TypeVar("_T")

if TYPE_CHECKING:
    from typing_extensions import Protocol
    from typing import Union, Tuple, Dict

    _KT = TypeVar("_KT")
    _VT_co = TypeVar("_VT_co", covariant=True)
    _ST1 = TypeVar("_ST1", bound="LazyImporter[Any]")
    _ST2 = TypeVar("_ST2", bound="MutableLazyImporter[Any]")

    class _SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):
        def __getitem__(self, __key: _KT) -> _VT_co: ...
        def keys(self) -> Iterable[_KT]: ...

    _DictLike = Union[_SupportsKeysAndGetItem[str, str], Iterable[Tuple[str, str]]]
    _ReduceTuple = Tuple[Callable[[str, Dict[str, str]], _T], Tuple[str, Dict[str, str]]]

__all__ = ["MutableLazyImporter", "LazyImporter"]


class _CustomRepr(reprlib.Repr):
    def __init__(self) -> None:
        super().__init__()
        self.maxdict = 2

    def repr_mappingproxy(self, x: types.MappingProxyType[Any, Any], level: int) -> str:
        return self.repr_dict(x, level)  # type: ignore[arg-type]


_repr = _CustomRepr().repr


[docs]class LazyImporter(Generic[_T]): """A class for lazilly importing objects. Parameters ---------- module : :class:`types.ModuleType` The to-be wrapped module. imports : :class:`Mapping[str, str]<collections.abc.Mapping>` A mapping that maps the names of to-be lazzily imported objects to the names of their modules. Examples -------- .. code-block:: python >>> from nanoutils import LazyImporter >>> __getattr__ = LazyImporter.from_name("nanoutils", {"Any": "typing"}) >>> print(__getattr__) LazyImporter(module=nanoutils, imports={'Any': 'typing'}) >>> __getattr__("Any") typing.Any """ __slots__ = ("__weakref__", "_imports", "_module", "_hash") @property def module(self) -> types.ModuleType: """:class:`types.ModuleType`: Get the wrapped module.""" return self._module @property def imports(self) -> Mapping[str, str]: """:class:`types.MappingProxyType[str, str]<types.MappingProxyType>`: Get a mapping that maps object names to their module name.""" # noqa: E501 return self._imports def __init__( self: LazyImporter[Any], module: types.ModuleType, imports: _DictLike, ) -> None: """Initialize a new instance.""" if not isinstance(module, types.ModuleType): raise TypeError(f"Expected a module, not {type(module).__name__}") self._module = module self._imports: Mapping[str, str] = types.MappingProxyType(dict(imports))
[docs] @classmethod def from_name(cls: Type[_ST1], name: str, imports: _DictLike) -> _ST1: """Construct a new instance from the module **name**. Parameters ---------- name : :class:`str` The name of the to-be wrapped module. imports : :class:`Mapping[str, str]<collections.abc.Mapping>` A mapping that maps the names of to-be lazzily imported objects to the names of their modules. Returns ------- :class:`nanoutils.LazyImporter` A new LazyImporter instance or a subclass thereof. """ if not isinstance(name, str): raise TypeError(f"Expected a string, not {type(name).__name__}") module = sys.modules.get(name) # fast-path if module is None: module = importlib.import_module(name) return cls(module, imports)
def __reduce__(self: _ST1) -> _ReduceTuple[_ST1]: """Helper for :mod:`pickle`.""" cls = type(self) args = (self.module.__name__, self.imports.copy()) # type: ignore[attr-defined] return cls.from_name, args def __copy__(self: _ST1) -> _ST1: """Implement :func:`copy.copy(self)<copy.copy>`.""" return self def __deepcopy__(self: _ST1, memo: None | dict[int, Any] = None) -> _ST1: """Implement :func:`copy.deepcopy(self, memo=memo)<copy.deepcopy>`.""" return self def __hash__(self) -> int: """Implement :func:`hash(self)<hash>`.""" try: return self._hash except AttributeError: self._hash: int = hash(self._module) ^ hash(frozenset(self.imports.items())) return self._hash def __eq__(self, value: object) -> bool: """Implement :meth:`self == value<object.__eq__>`.""" if not isinstance(value, LazyImporter): return NotImplemented return self.module is value.module and self.imports == value.imports def __repr__(self) -> str: """Implement :func:`repr(self)<repr>`.""" name = type(self).__name__ return f"{name}(module={self.module.__name__}, imports={_repr(self.imports)})" def __call__(self, name: str) -> _T: """Implement :func:`getattr(self.module, name)<getattr>`.""" # Get the module-name associated with `name` try: module_name = self.imports[name] except KeyError: raise AttributeError( f"module {self.module.__name__!r} has no attribute {name!r}" ) from None # Import the module module = sys.modules.get(module_name) if module is None: module = importlib.import_module(module_name) # Extract `name` from `module` ret: _T = getattr(module, name) setattr(self.module, name, ret) return ret
[docs]class MutableLazyImporter(LazyImporter[_T]): """A subclass of :class:`~nanoutils.LazyImporter` with mutable :attr:`imports`. Parameters ---------- module : :class:`types.ModuleType` The to-be wrapped module. imports : :class:`Mapping[str, str]<collections.abc.Mapping>` A mapping that maps the names of to-be lazzily imported objects to the names of their modules. Examples -------- .. code-block:: python >>> from nanoutils import MutableLazyImporter >>> __getattr__ = MutableLazyImporter.from_name("nanoutils", {"Any": "typing"}) >>> print(__getattr__) MutableLazyImporter(module=nanoutils, imports={'Any': 'typing'}) >>> __getattr__("Any") typing.Any >>> del __getattr__.imports["Any"] >>> print(__getattr__) MutableLazyImporter(module=nanoutils, imports={}) >>> __getattr__.imports = {"Hashable": "collections.abc"} >>> __getattr__("Hashable") <class 'collections.abc.Hashable'> """ __hash__ = None # type: ignore[assignment] @property def imports(self) -> dict[str, str]: """:class:`dict[str, str]<dict>`: Get or set the dictionary that maps object names to their module name. Setting a value will assign it as a copy. """ # noqa: E501 return self._imports @imports.setter def imports(self, value: _DictLike) -> None: self._imports = dict(value) def __init__( self: MutableLazyImporter[Any], module: types.ModuleType, imports: _DictLike, ) -> None: """Initialize a :class:`MutableLazyImporter` instance.""" if not isinstance(module, types.ModuleType): raise TypeError(f"Expected a module, not {module.__class__.__name__}") self._module = module self._imports: dict[str, str] = dict(imports) def __copy__(self: _ST2) -> _ST2: """Implement :func:`copy.copy(self)<copy.copy>`.""" cls = type(self) ret: _ST2 = object.__new__(cls) ret._module = self.module ret._imports = self.imports return ret def __deepcopy__(self: _ST2, memo: None | dict[int, Any] = None) -> _ST2: """Implement :func:`copy.deepcopy(self, memo=memo)<copy.deepcopy>`.""" cls = type(self) ret: _ST2 = object.__new__(cls) ret._module = self.module ret._imports = self.imports.copy() return ret def __reduce__(self: _ST2) -> _ReduceTuple[_ST2]: """Helper for :mod:`pickle`.""" cls = type(self) return cls.from_name, (self.module.__name__, self.imports)