Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/source/pcapkit/corekit/fields/numbers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ Internal Definitions

.. autofunction:: pcapkit.corekit.fields.numbers._rebuild_unregistered_member

.. autofunction:: pcapkit.corekit.fields.numbers._reduce_unregistered_member
.. versionremoved:: 1.6.0

``pcapkit.corekit.fields.numbers._reduce_unregistered_member``, with no
alias; :func:`pcapkit.utilities.pickle.reduce_to` installs the reduction it
built (:issue:`2714`).

.. autoclass:: pcapkit.corekit.fields.numbers._CapturedInt
:no-members:
Expand Down
8 changes: 5 additions & 3 deletions docs/source/pcapkit/utilities/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ Module Utilities
:mod:`pcapkit.utilities` holds the cross-cutting services :mod:`pcapkit` is
built with: user-refined exceptions and warnings, logging, the tunable limits
of :mod:`~pcapkit.utilities.config`, a compatibility layer, timestamp
conversion, the :mod:`context variables <pcapkit.utilities.contextvars>` its
modules share, and wrappers around third-party libraries. It imports nothing from
:mod:`pcapkit` outside itself; see :ref:`utilities-or-corekit`.
conversion, pickle reductions, the
:mod:`context variables <pcapkit.utilities.contextvars>` its modules share, and
wrappers around third-party libraries. It imports nothing from :mod:`pcapkit`
outside itself; see :ref:`utilities-or-corekit`.

.. toctree::
:maxdepth: 2
Expand All @@ -20,6 +21,7 @@ modules share, and wrappers around third-party libraries. It imports nothing fro
config
chardet
datetime
pickle
contextvars

Version Compatibility
Expand Down
27 changes: 27 additions & 0 deletions docs/source/pcapkit/utilities/pickle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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` lets a :class:`typing.Generic` subclass or a slotted
:class:`dict` pickle 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; and :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.

.. autoclass:: pcapkit.utilities.pickle.ReduceAsProtocol2
:members: __reduce_ex__

.. autoclass:: pcapkit.utilities.pickle.ReduceToName
:members: __reduce__

.. autoclass:: pcapkit.utilities.pickle.ReduceToItems
:members: __reduce_ex__

.. autofunction:: pcapkit.utilities.pickle.reduce_to

.. autofunction:: pcapkit.utilities.pickle._reduce_to
38 changes: 7 additions & 31 deletions pcapkit/corekit/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,16 @@
an unknown value does not mint one (:issue:`775`).

"""
import functools
from typing import TYPE_CHECKING

from aenum import extend_enum

from pcapkit.corekit.sentinels import NO_DEFAULT
from pcapkit.utilities.exceptions import BaseError, CaughtByCaller, EnumKeyError, EnumValueError
from pcapkit.utilities.pickle import reduce_to

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

from typing_extensions import Self

Expand Down Expand Up @@ -730,11 +730,10 @@ def _make_reducible(cls, obj: 'Any', *args: 'Any') -> 'Any':
lookup table grows. The rebuilt member is itself reducible, since the
method it comes back through installs this again.

The override goes on the *instance* rather than on the type, which both
:mod:`pickle` and :mod:`copy` honour: each fetches ``__reduce_ex__``
with :func:`getattr` on the object. :func:`functools.partial` rather
than a closure keeps the member itself out of its own reducer, so being
reducible does not put it in a reference cycle.
The override goes on the *instance* rather than on the type, through
:func:`~pcapkit.utilities.pickle.reduce_to`, which :mod:`pickle` and
:mod:`copy` both honour, and which keeps the member out of its own
reducer.

Note:
This is opt-in per registry rather than done by
Expand All @@ -757,8 +756,7 @@ def _make_reducible(cls, obj: 'Any', *args: 'Any') -> 'Any':
``obj``, for use as a ``return`` expression.

"""
obj.__reduce_ex__ = functools.partial(_reduce_unregistered_member, cls, args)
return obj
return reduce_to(obj, _rebuild_unregistered_member, cls, args)


def _rebuild_unregistered_member(namespace: 'Type[EnumRegistry]',
Expand All @@ -783,25 +781,3 @@ def _rebuild_unregistered_member(namespace: 'Type[EnumRegistry]',
return namespace._unregistered_member(*args) # pylint: disable=protected-access


def _reduce_unregistered_member( # pylint: disable=unused-argument
namespace: 'Type[EnumRegistry]', args: 'tuple[Any, ...]',
protocol: 'int') -> 'tuple[Callable[..., Any], tuple[Any, ...]]':
"""The ``__reduce_ex__`` :meth:`EnumRegistry._make_reducible` installs.

Bound to its first two arguments with :func:`functools.partial`, so that
the reducer holds the ingredients of the member rather than the member
itself.

Args:
namespace: The registry class the member is an instance of.
args: The arguments to rebuild the member from.
protocol: The :mod:`pickle` protocol version, as passed by
:mod:`pickle` and :mod:`copy`. Unused -- the reduction is the same
for every protocol -- but accepted because both call this with it.

Returns:
The callable and arguments :mod:`pickle` and :mod:`copy` should rebuild
the member through.

"""
return (_rebuild_unregistered_member, (namespace, args))
48 changes: 13 additions & 35 deletions pcapkit/corekit/fields/numbers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# -*- coding: utf-8 -*-
"""numerical field class"""
"""numerical field class

.. versionremoved:: 1.6.0

``pcapkit.corekit.fields.numbers._reduce_unregistered_member``, with no
alias; :func:`pcapkit.utilities.pickle.reduce_to` installs the reduction it
built (:issue:`2714`).

"""

import enum
import functools
import math
from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast

Expand All @@ -11,6 +18,7 @@
from pcapkit.corekit.fields.field import Field
from pcapkit.corekit.sentinels import NO_VALUE
from pcapkit.utilities.exceptions import BaseError, FieldValueError, IntError, ProtocolError
from pcapkit.utilities.pickle import reduce_to

__all__ = [
'NumberField',
Expand Down Expand Up @@ -958,14 +966,9 @@ def _unregistered_member(namespace: 'Type[StdlibEnum] | Type[AenumEnum]',
# NOTE: ``Enum.__reduce_ex__`` reduces a member to ``(cls, (value,))``,
# i.e. to the one lookup this member is deliberately absent from, so
# pickle and copy would both raise on it without this. Overriding it
# per instance -- which pickle and copy both honour, since both fetch
# it with getattr on the object rather than on its type -- rebuilds an
# equivalent unregistered member instead. functools.partial rather
# than a closure keeps this off obj itself, so the member does not
# become part of a reference cycle merely by being reducible.
obj.__reduce_ex__ = functools.partial(
_reduce_unregistered_member, namespace, value, name, attrs)
return obj
# per instance, which pickle and copy both honour, rebuilds an
# equivalent unregistered member instead.
return reduce_to(obj, _rebuild_unregistered_member, namespace, value, name, attrs)


def _rebuild_unregistered_member(namespace: 'Type[StdlibEnum] | Type[AenumEnum]',
Expand Down Expand Up @@ -996,31 +999,6 @@ def _rebuild_unregistered_member(namespace: 'Type[StdlibEnum] | Type[AenumEnum]'
namespace, value, name, **attrs)


def _reduce_unregistered_member( # pylint: disable=unused-argument
namespace: 'Type[StdlibEnum] | Type[AenumEnum]',
value: 'Any', name: 'str', attrs: 'dict[str, Any]',
protocol: 'int') -> 'tuple[Callable[..., Any], tuple[Any, ...]]':
"""The ``__reduce_ex__`` :meth:`EnumField._unregistered_member` installs.

Bound to its first four arguments with :func:`functools.partial`, so that
the reducer holds the ingredients of the member rather than the member
itself.

Args:
namespace: The registry class the member is an instance of.
value: The member's ``_value_``.
name: The member's ``_name_``.
attrs: The extra attributes the member carries.
protocol: The :mod:`pickle` protocol version, ignored -- the reduction
is the same for all of them, and :mod:`copy` passes 4 here.

Returns:
A two-tuple of :func:`_rebuild_unregistered_member` and its arguments.

"""
return (_rebuild_unregistered_member, (namespace, value, name, attrs))


class PortEnumField(EnumField):
"""Enumerated value for transport-layer port fields.

Expand Down
8 changes: 3 additions & 5 deletions pcapkit/corekit/infoclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict, iter_multi_items
from pcapkit.utilities.compat import Mapping, UnionType, final, get_annotations
from pcapkit.utilities.exceptions import InfoError, UnsupportedCall, stacklevel
from pcapkit.utilities.pickle import ReduceAsProtocol2, ReduceToItems
from pcapkit.utilities.warnings import InfoWarning, warn

if TYPE_CHECKING:
Expand Down Expand Up @@ -318,7 +319,7 @@ def __new__(cls, name: 'str', bases: 'tuple[type, ...]', attrs: 'dict[str, Any]'
return super().__new__(cls, name, bases, attrs, **kwargs) # type: ignore[return-value]


class Info(Mapping[str, VT], Generic[VT], metaclass=InfoMeta):
class Info(ReduceAsProtocol2, Mapping[str, VT], Generic[VT], metaclass=InfoMeta):
"""Turn dictionaries into :obj:`object` like instances.

* :class:`Info` objects are :class:`~collections.abc.Mapping` instances,
Expand Down Expand Up @@ -975,7 +976,7 @@ def _immutable(self, *args: 'Any', **kwargs: 'Any') -> 'NoReturn':
__setstate__ = _immutable


class MultiInfo(_MultiInfo[KT, VT], MultiDict[KT, VT]):
class MultiInfo(ReduceToItems, _MultiInfo[KT, VT], MultiDict[KT, VT]):
"""An immutable :class:`~pcapkit.corekit.multidict.MultiDict` that is an :class:`Info`.

It holds every value of a key, as a
Expand Down Expand Up @@ -1065,9 +1066,6 @@ def listvalues(self) -> 'Iterator[list[VT]]':
"""
return (values for (_, values) in self.lists())

def __reduce_ex__(self, protocol: 'Any') -> 'tuple[type, tuple[list[tuple[KT, VT]]]]':
return type(self), (list(self.items(multi=True)),)


class OrderedMultiInfo(_MultiInfo[KT, VT], OrderedMultiDict[KT, VT]):
"""An immutable :class:`~pcapkit.corekit.multidict.OrderedMultiDict` that is an :class:`Info`.
Expand Down
15 changes: 6 additions & 9 deletions pcapkit/corekit/multidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from typing import TYPE_CHECKING, Generic, TypeVar, overload

from pcapkit.utilities.exceptions import MissingKeyError, UnsupportedCall
from pcapkit.utilities.pickle import ReduceToItems, ReduceToName

if TYPE_CHECKING:
from typing import Any, Iterable, Iterator, Mapping, NoReturn, Optional, SupportsIndex
from typing import Any, Iterable, Iterator, Mapping, NoReturn, Optional

from typing_extensions import Literal, Self

Expand Down Expand Up @@ -68,13 +69,12 @@ def unlink(self, omd: 'OrderedMultiDict[_KT, _VT]') -> 'None':
omd._last_bucket = self.prev # pylint: disable=protected-access


class _Missing:
class _Missing(ReduceToName):
_name = '_missing'

def __repr__(self) -> 'str':
return "no value"

def __reduce__(self) -> 'str':
return "_missing"


_missing = _Missing()

Expand Down Expand Up @@ -470,7 +470,7 @@ def __repr__(self) -> 'str':
return f"{type(self).__name__}({list(self.items(multi=True))!r})"


class OrderedMultiDict(MultiDict[_KT, _VT]):
class OrderedMultiDict(ReduceToItems, MultiDict[_KT, _VT]):
"""Works like a regular :class:`MultiDict` but preserves the
order of the fields. To convert the ordered multi dict into a
list you can use the :meth:`items` method and pass it ``multi=True``.
Expand Down Expand Up @@ -537,9 +537,6 @@ def deepcopy(self, memo: 'Optional[dict]' = None) -> 'OrderedMultiDict[_KT, _VT]
"""
return self.__class__(copy.deepcopy(list(self.items(multi=True)), memo=memo))

def __reduce_ex__(self, protocol: 'SupportsIndex') -> 'Any':
return type(self), (list(self.items(multi=True)),)

def __getstate__(self) -> 'Any':
return list(self.items(multi=True))

Expand Down
16 changes: 7 additions & 9 deletions pcapkit/corekit/sentinels.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from typing import TYPE_CHECKING, cast

from pcapkit.utilities.compat import final
from pcapkit.utilities.pickle import ReduceToName

__all__ = ['Sentinel', 'NULL', 'NO_VALUE', 'NO_DEFAULT']

Expand Down Expand Up @@ -197,7 +198,7 @@ def register(cls, subclass: 'type[Any]') -> 'type[Any]':
return super().register(subclass)


class Sentinel(metaclass=SentinelMeta):
class Sentinel(ReduceToName, metaclass=SentinelMeta):
"""Abstract base of every sentinel type, so each behaves like the type of :obj:`None`.

A subclass sets ``_name`` to the name of the module-level variable that
Expand All @@ -210,10 +211,11 @@ class Sentinel(metaclass=SentinelMeta):
:obj:`None`, so a caller who builds one by mistake still gets the
sentinel.
* :meth:`__copy__` and :meth:`__deepcopy__` return ``self``.
* :meth:`__reduce__` returns ``_name``, as ``Ellipsis.__reduce__()``
returns ``'Ellipsis'``, so :mod:`pickle` stores the instance as a
reference to that module-level name and loads it back as the same
object, at every protocol. Guarding :meth:`__new__` alone would not
* :meth:`~pcapkit.utilities.pickle.ReduceToName.__reduce__`, from
:class:`~pcapkit.utilities.pickle.ReduceToName`, returns ``_name``, as
``Ellipsis.__reduce__()`` returns ``'Ellipsis'``, so :mod:`pickle` stores
the instance as a reference to that module-level name and loads it back
as the same object, at every protocol. Guarding :meth:`__new__` alone would not
cover protocols 0 and 1, which rebuild through
:func:`copyreg._reconstructor` and so call :meth:`object.__new__`
directly.
Expand Down Expand Up @@ -318,10 +320,6 @@ def __deepcopy__(self, memo: 'dict[int, Any]') -> 'Self':
"""
return self

def __reduce__(self) -> 'str':
"""Reduce to ``_name``, which :mod:`pickle` stores as a global reference."""
return self._name


@final
class NullType(Sentinel):
Expand Down
Loading
Loading