Source code for pcapkit.corekit.packet

# -*- coding: utf-8 -*-
"""Deferred Packet
===================

.. module:: pcapkit.corekit.packet

:mod:`pcapkit.corekit.packet` contains
:class:`~pcapkit.corekit.packet.DeferredPacket`, the mixin that lets an
:class:`~pcapkit.corekit.infoclass.Info` subclass hold its ``packet`` field as
a placeholder and resolve it on first read. A reassembled datagram's ``packet``
is a second parse of its payload, and a traced flow's is a reassembly of its
stream; most callers want neither, so both are postponed until somebody reads
them.

A subclass keeps a contract, which
:meth:`DeferredPacket.__init_subclass__` checks at class creation:

* It is declared ``class X(DeferredPacket, Info)``, mixin first, so the methods
  here take precedence over :class:`~pcapkit.corekit.infoclass.Info`'s own.
* It lists ``packet`` in its ``__additional__``. That is what makes the field
  lazy at all: :class:`~pcapkit.corekit.infoclass.Info` stores a field named
  there under a mangled key and maps it back on the way out, so ``packet``
  never lands in :attr:`~object.__dict__` under its own name. Reading it
  therefore reaches :meth:`DeferredPacket.__getattr__`, where the placeholder
  can be resolved, while ``dict(x)``, :meth:`DeferredPacket.to_dict` and
  iteration still report the field as ``packet``.
* It names its placeholder class in
  :attr:`~DeferredPacket.__deferred__`. A value of that class is called with no
  arguments to produce the real ``packet``; any other value already is one.

Resolution happens **at most once**: the result replaces the placeholder in
:attr:`~object.__dict__`, so every later read returns that same object and the
placeholder is never called again.

This module imports nothing from :mod:`pcapkit.foundation` or
:mod:`pcapkit.protocols`. Each placeholder class stays with the subsystem that
creates it, and only the subclass knows which one it holds.

"""
from typing import TYPE_CHECKING

from pcapkit.utilities.exceptions import InfoError

if TYPE_CHECKING:
    from typing import Any, Callable, ClassVar, Type

__all__ = ['DeferredPacket']


[docs] class DeferredPacket: """Resolves a deferred ``packet`` field on first read. A mixin for :class:`~pcapkit.corekit.infoclass.Info` subclasses; see :mod:`pcapkit.corekit.packet` for the contract a subclass keeps. Reading ``packet`` -- as an attribute, as ``x['packet']``, or through :meth:`to_dict`, :func:`str` or :func:`repr` -- resolves a placeholder once and keeps the result. ``'packet' in x``, :func:`len` and iteration report the field by name without resolving it. It is shared by :class:`pcapkit.foundation.reassembly.data.ip.Datagram`, :class:`pcapkit.foundation.reassembly.data.tcp.Datagram` and :class:`pcapkit.foundation.traceflow.data.tcp.Index`, which differ only in the placeholder class each names in :attr:`__deferred__` (:issue:`1516`). """ if TYPE_CHECKING: #: Placeholder class the ``packet`` field may hold, set by each subclass. #: A value of this class is resolved by calling it with no arguments; #: an instance of any other class, :data:`None` included, is not. __deferred__: 'ClassVar[Type[Callable[[], Any]]]' # NOTE: the ``super()`` calls below are suppressed for both checkers. They are # undefined *on this mixin*, which is what a mixin is -- the base arrives at # the point of use, where every subclass is declared # ``class X(DeferredPacket, Info)`` and :class:`~pcapkit.corekit.infoclass.Info` # supplies each of them. Neither mypy nor pylint can see that from here, and # pylint calls it an *error* rather than a warning.
[docs] def __init_subclass__(cls, /, *args: 'Any', **kwargs: 'Any') -> 'None': """Refuse a subclass that does not keep the contract. Either omission would otherwise fail silently, and only on a read. Without ``packet`` in ``__additional__`` the field lands in :attr:`~object.__dict__` under its own name, :meth:`__getattr__` is never reached, and the placeholder itself comes back. Without :attr:`__deferred__`, :meth:`__analyse__` raises :exc:`AttributeError` from inside :meth:`__getattr__`, which ``getattr(x, 'packet', None)`` takes for a missing attribute and answers with :data:`None`. Args: *args: Arbitrary positional arguments. **kwargs: Arbitrary keyword arguments in class definition. Raises: InfoError: If ``cls`` does not list ``packet`` in its ``__additional__``, or does not name a class in its :attr:`__deferred__`. """ super().__init_subclass__(*args, **kwargs) if 'packet' not in getattr(cls, '__additional__', ()): raise InfoError(f"{cls.__name__}: 'packet' is not listed in __additional__, " 'so reading it would never resolve the placeholder') if not isinstance(getattr(cls, '__deferred__', None), type): raise InfoError(f"{cls.__name__}: __deferred__ does not name the placeholder class " "that 'packet' may hold")
[docs] def __analyse__(self) -> 'Any': """Resolve the ``packet`` field, at most once. A placeholder, i.e. an instance of :attr:`__deferred__`, is called with no arguments and its result replaces it in :attr:`~object.__dict__`. Any other value is returned as it is. Returns: The resolved ``packet``, which may be :data:`None` when the subclass stored that instead of a placeholder. """ key = self.__map__.get('packet', 'packet') value = self.__dict__[key] if isinstance(value, self.__deferred__): value = value() self.__dict__[key] = value return value
def __getattr__(self, name: 'str') -> 'Any': # NOTE: reached only for names absent from ``__dict__``, which ``packet`` # always is -- see ``__additional__`` in the module docstring. Everything # else has to raise, or a typo would silently answer with the resolved # ``packet``. if name != 'packet': raise AttributeError(f'{type(self).__name__!r} object has no attribute {name!r}') return self.__analyse__() def __getitem__(self, name: 'str') -> 'Any': if name == 'packet': return self.__analyse__() return super().__getitem__(name) # type: ignore[misc] # pylint: disable=no-member def __contains__(self, name: 'object') -> 'bool': # NOTE: ``Mapping.__contains__`` answers by fetching the value, which # would resolve the placeholder merely to decide that the field exists. # ``packet`` is a declared field, so it is always there. return name == 'packet' or super().__contains__(name) # type: ignore[misc] # pylint: disable=no-member def __str__(self) -> 'str': self.__analyse__() return super().__str__() def __repr__(self) -> 'str': self.__analyse__() return super().__repr__()
[docs] def to_dict(self) -> 'dict[str, Any]': """Convert the instance into :obj:`dict`. Returns: The fields, with ``packet`` resolved if it had not been read yet -- a :obj:`dict` holding the placeholder would leak an implementation detail into what is meant to be plain data. """ self.__analyse__() return super().to_dict() # type: ignore[misc] # pylint: disable=no-member