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