Source code for pcapkit.utilities.pickle

# -*- coding: utf-8 -*-
"""Pickle Reductions
=======================

.. module:: pcapkit.utilities.pickle

:mod:`pcapkit.utilities.pickle` holds the :mod:`pickle` reductions several
:mod:`pcapkit` classes share, so each is written once (:issue:`2714`):

* :class:`ReduceAsProtocol2` reduces under every protocol as protocol 2 does, so a
  :class:`typing.Generic` subclass or a class with ``__slots__`` pickles under
  protocol 0 and 1, on GraalPy as on CPython;
* :class:`ReduceToName` reduces a singleton to the module-level name holding it;
* :class:`ReduceToItems` reduces a multi-valued mapping to a call of its class on
  its item pairs;
* :func:`reduce_to` makes one instance reduce to a call of a given function.

A pickle a class wrote before it took one of these still loads, and one it writes
with it loads where the class still reduces by its own.

"""

import functools
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Any, Callable, SupportsIndex, TypeVar

    _T = TypeVar('_T')

__all__ = ['ReduceAsProtocol2', 'ReduceToName', 'ReduceToItems', 'reduce_to']


[docs] class ReduceAsProtocol2: """Mixin reducing an instance under every :mod:`pickle` protocol as protocol 2 does. Below protocol 2, :meth:`object.__reduce_ex__` hands the instance to ``copyreg._reduce_ex``, which walks ``type(self).__mro__`` for the first base that is not a heap type and, unless that base is :class:`object`, calls it on the instance. :class:`~typing.Generic` is a heap type on CPython, so the walk passes it. GraalPy builds ``Generic`` as a built-in, so the walk stops there and raises ``TypeError: typing.Generic() takes no arguments`` for any generic class with no reduction of its own (:issue:`2714`). The same walk refuses a class with ``__slots__`` and no ``__getstate__`` of its own on every interpreter. Protocol 2's reduction reads the class and the instance state without that walk, slots included, and protocol 0 and 1 write it as an ordinary call of ``copyreg.__newobj__``, restoring the instance through ``__new__`` as protocol 2 does. The next ``__reduce_ex__`` in the MRO builds it, so a base with a reduction of its own keeps it, as :class:`~pcapkit.corekit.infoclass.OrderedMultiInfo` keeps :class:`~pcapkit.corekit.multidict.OrderedMultiDict`'s, and a subclass overriding ``__reduce__`` keeps its own. It goes before :class:`~typing.Generic` among the bases, as on :class:`~pcapkit.corekit.infoclass.Info`, :class:`~pcapkit.protocols.schema.schema.Schema`, :class:`~pcapkit.foundation.reassembly.reassembly.ReassemblyBase` and :class:`~pcapkit.foundation.reassembly.data.data.Deferred`. .. versionadded:: 1.6.0 """ __slots__ = ()
[docs] def __reduce_ex__(self, protocol: 'SupportsIndex') -> 'str | tuple[Any, ...]': """Reduce as protocol 2 does, or as ``protocol`` does if higher. Args: protocol: Pickle protocol in use. Returns: The reduction for ``protocol``, or for protocol 2 below it. """ return super().__reduce_ex__(max(int(protocol), 2))
[docs] class ReduceToName: """Mixin reducing a singleton to the module-level name that holds it. :meth:`__reduce__` returns ``_name``, as ``Ellipsis.__reduce__()`` returns ``'Ellipsis'``, so :mod:`pickle` stores the instance as a reference to that name in the class's module and loads it back as the same object, at every protocol. A class using it sets ``_name`` to that name. It is shared by every :class:`~pcapkit.corekit.sentinels.Sentinel` and by the missing-value marker of :mod:`pcapkit.corekit.multidict`. .. versionadded:: 1.6.0 """ __slots__ = ()
[docs] def __reduce__(self) -> 'str': """Reduce to ``_name``, which :mod:`pickle` stores as a global reference.""" return self._name # type: ignore[attr-defined] # pylint: disable=no-member
[docs] class ReduceToItems: """Mixin reducing a multi-valued mapping to a call of its class on its item pairs. :meth:`__reduce_ex__` returns ``(type(self), (pairs,))``, the pairs being ``list(self.items(multi=True))``, under every protocol. Rebuilding through the class keeps the order of interleaved keys and builds whatever the class keeps beside the pairs, and needs no state to be restored afterwards, which an immutable class refuses. It is shared by :class:`~pcapkit.corekit.multidict.OrderedMultiDict` and :class:`~pcapkit.corekit.infoclass.MultiInfo`. .. versionadded:: 1.6.0 """ __slots__ = ()
[docs] def __reduce_ex__(self, protocol: 'SupportsIndex') -> 'str | tuple[Any, ...]': """Reduce to a call of the class on the item pairs, under every protocol. Args: protocol: Pickle protocol in use, which does not change the reduction. Returns: ``(type(self), (list(self.items(multi=True)),))``. """ return type(self), (list(self.items(multi=True)),) # type: ignore[attr-defined] # pylint: disable=no-member
[docs] def reduce_to(obj: '_T', rebuild: 'Callable[..., Any]', *args: 'Any') -> '_T': """Make ``obj`` reduce to ``rebuild(*args)`` under :mod:`pickle` and :mod:`copy`. The reduction goes on the *instance*, as ``obj.__reduce_ex__``, which both :mod:`pickle` and :mod:`copy` honour, since each fetches it with :func:`getattr` on the object. That suits an object whose type reduces it wrongly, as ``Enum.__reduce_ex__`` reduces an unregistered member to the one value lookup it is absent from. :func:`functools.partial` rather than a closure keeps ``obj`` out of its own reducer, so being reducible does not put it in a reference cycle. It is shared by :meth:`EnumRegistry._make_reducible <pcapkit.corekit.enum.EnumRegistry._make_reducible>` and :meth:`EnumField._unregistered_member <pcapkit.corekit.fields.numbers.EnumField._unregistered_member>`. Args: obj: The object to make reducible. rebuild: A module-level callable, which every :mod:`pickle` protocol can name, rebuilding an equivalent object from ``args``. *args: The arguments to ``rebuild``. Returns: ``obj``, for use as a ``return`` expression. .. versionadded:: 1.6.0 """ obj.__reduce_ex__ = functools.partial(_reduce_to, rebuild, args) # type: ignore[method-assign] return obj
[docs] def _reduce_to( # pylint: disable=unused-argument rebuild: 'Callable[..., Any]', args: 'tuple[Any, ...]', protocol: 'int') -> 'tuple[Callable[..., Any], tuple[Any, ...]]': """The ``__reduce_ex__`` :func:`reduce_to` installs, bound to its first two arguments. Args: rebuild: The callable to rebuild through. args: Its arguments. protocol: Pickle protocol in use, ignored; :mod:`copy` passes 4. Returns: ``(rebuild, args)``. """ return rebuild, args