Source code for pcapkit.utilities.importlib

# -*- coding: utf-8 -*-
"""Lazy Imports
==================

.. module:: pcapkit.utilities.importlib

:mod:`pcapkit.utilities.importlib` holds :func:`lazy_exports`, which gives a package
the :pep:`562` ``__getattr__`` and ``__dir__`` that import each name it exports
the first time the name is reached, rather than with the package itself. Every
package under :mod:`pcapkit.const` imports its enumerations that way
(:issue:`2729`), the mechanism :mod:`pcapkit.const.reg.apptype` had for its TCP
and UDP registries (:issue:`1538`) made general.

.. versionadded:: 1.6.0

"""
import importlib
import types
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Any, Callable, Iterable, Mapping, Optional

__all__ = ['lazy_exports']


[docs] def lazy_exports(package: 'str', namespace: 'dict[str, Any]', exports: 'Optional[Mapping[str, tuple[str, str]]]' = None, *, packages: 'Iterable[str]' = ()) -> 'tuple[Callable[[str], Any], Callable[[], list[str]]]': """Return the ``__getattr__`` and ``__dir__`` of a package that imports its exports on first use. A package binds both at module level, after its ``__all__``, and imports the exported names only under :data:`~typing.TYPE_CHECKING`, for the type checkers: .. code-block:: python if TYPE_CHECKING: from pcapkit.const.tcp.option import Option as TCP_Option __all__ = ['TCP_Option'] __getattr__, __dir__ = lazy_exports(__name__, globals(), { 'TCP_Option': ('option', 'Option'), }) :pep:`562` calls ``__getattr__`` only for a name missing from the package's globals. For an exported name it imports the submodule, reads the attribute off it and binds it in ``namespace``, so it runs once per name; for the name of a submodule an export comes from, it imports that submodule, which the import system binds on the package. So ``from package import Name``, ``import package.submodule``, attribute access, ``import *`` and the :mod:`pickle` of an object defined in a submodule all behave as an eager import would, apart from *when* the import happens. ``__dir__`` lists what the eager import would have bound: the names the import system sets, ``__all__``, every export, every submodule an export comes from, and any other submodule already imported. The package's own helpers -- ``TYPE_CHECKING``, this function, the two hooks -- are left out. Args: package: The package's ``__name__``. namespace: The package's ``globals()``, which ``__getattr__`` binds each name in once it is imported. exports: The names imported on first use, each mapped to the submodule it is defined in, relative to ``package``, and its name there. packages: Subpackages, relative to ``package``, whose ``__all__`` this package re-exports. Each is imported by this call -- one using :func:`lazy_exports` itself imports none of its exports -- and every name in its ``__all__`` is read off it on first use. Returns: The package's ``__getattr__`` and ``__dir__``. """ table = dict(exports or {}) for name in packages: module = importlib.import_module(f'{package}.{name}') table.update((export, (name, export)) for export in module.__all__) submodules = {module for module, _ in table.values()} def __getattr__(name: 'str') -> 'Any': if name in table: module, attribute = table[name] value = getattr(importlib.import_module(f'{package}.{module}'), attribute) namespace[name] = value return value if name in submodules: return importlib.import_module(f'{package}.{name}') raise AttributeError(f'module {package!r} has no attribute {name!r}') def __dir__() -> 'list[str]': names = {name for name in namespace if name.startswith('__') and name.endswith('__')} names -= {'__getattr__', '__dir__'} names.update(namespace.get('__all__', ()), table, submodules) names.update(name for (name, value) in list(namespace.items()) if isinstance(value, types.ModuleType) and value.__name__ == f'{package}.{name}') return sorted(names) return __getattr__, __dir__