Source code for pcapkit.corekit.module

# -*- coding: utf-8 -*-
"""Module Descriptor
=======================

.. module:: pcapkit.corekit.module

:mod:`pcapkit.corekit.module` contains :obj:`tuple`
like class :class:`~pcapkit.corekit.module.ModuleDescriptor`,
which is originally designed as :obj:`tuple[str, str] <tuple>`.

"""
import collections
import importlib
import sys
from typing import TYPE_CHECKING, Generic, TypeVar, cast

from pcapkit.corekit.sentinels import NULL
from pcapkit.utilities.exceptions import ProtocolError

__all__ = ['ModuleDescriptor']

if TYPE_CHECKING:
    from typing import Type

    from pcapkit.corekit.sentinels import NullType

_T = TypeVar('_T')


[docs] class ModuleDescriptor(collections.namedtuple('ModuleDescriptor', ['module', 'name']), Generic[_T]): """Module descriptor contains module name and class name, the actual class can be imported by ``from module import name``.""" __slots__ = () #: Module name. module: str #: Class name, or :data:`~pcapkit.corekit.sentinels.NULL` when whatever #: built this descriptor never got one -- see :attr:`klass`. name: 'str | NullType' @property def klass(self) -> 'Type[_T]': """Import class from module. .. versionchanged:: 1.5.0 A missing class raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than a bare :exc:`AttributeError` (:issue:`832`). Raises: pcapkit.utilities.exceptions.ProtocolError: If :attr:`name` is :data:`~pcapkit.corekit.sentinels.NULL` -- the caller building this descriptor omitted the class name rather than naming one that turned out wrong -- or if :attr:`module` has no attribute named :attr:`name`. Important: The module is read from :data:`sys.modules` first, and :func:`importlib.import_module` is entered only when it is not loaded yet. That matters because this property is on a *per-frame* dispatch path: :meth:`ProtocolBase._lookup_next_layer <pcapkit.protocols.protocol.ProtocolBase._lookup_next_layer>` deliberately writes no resolved class back, so every next layer lookup resolves its descriptor again -- the one registered for the code, or the fallback for :class:`~pcapkit.protocols.misc.raw.Raw` when nobody registered it. An extraction of :file:`many_interfaces.pcapng` performs 296 resolutions, 48 of them fallbacks (:issue:`1559`). :func:`~importlib.import_module` keeps real per-call work for an already-imported module (locks, :class:`~importlib.machinery.ModuleSpec` checks, the ``fromlist`` walk), so each repeat costs ~436 ns through it against ~117 ns through this property. The class is still read off the module with :func:`getattr` on every access, and nothing is memoised here. That is the point: :data:`sys.modules` *is* the module cache, and it is the only one whose invalidation the interpreter maintains -- :func:`importlib.reload` rebinds the class in place and ``sys.modules.pop()`` drops the entry, both of which this sees immediately. Holding the resolved class instead would serve the pre-reload class forever, and an instance of it fails :func:`isinstance` against the live one. """ if self.name is NULL: # ``module`` is a ``str`` and the caller never named a class -- # GitHub issue #832. Reporting that plainly, before ``getattr`` # ever sees it, is more useful than letting the sentinel reach # ``getattr``, which rejects a non-``str`` name with a bare # ``TypeError``. raise ProtocolError(f'missing class name for module {self.module!r}: pass an ' 'explicit class_ argument') # ``self.name`` is a ``str`` from here on -- the ``NULL`` case just # raised above -- so ``getattr`` below always gets a real name. name = cast('str', self.name) module = sys.modules.get(self.module) if module is not None: try: return getattr(module, name) except AttributeError: # ``sys.modules`` also holds modules whose body is still # executing -- a circular import, or another thread part way # through importing this one. ``import_module`` waits on the # per-module import lock, so defer to it rather than reporting # the attribute missing; a name that really is absent raises # from the ``getattr`` below instead, with the same message. pass try: return getattr(importlib.import_module(self.module), name) except AttributeError as error: # GitHub issue #832: a ``register_*`` helper that builds a # descriptor from a bad class name fails here with a bare stdlib # :exc:`AttributeError`, which a caller cannot catch as a # :mod:`pcapkit` error. Re-raised as :exc:`ProtocolError`, whose # message still names both the module and the missing class; only # the type changes. raise ProtocolError(str(error)) from error