Skip to content
Closed
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
11 changes: 11 additions & 0 deletions docs/source/pcapkit/corekit/multidict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ is inspired and based on the `Werkzeug`_ project.
:no-special-members: __init__
:show-inheritance:

Auxiliaries
-----------

.. autoclass:: pcapkit.corekit.multidict._Missing

.. automethod:: __bool__
.. automethod:: __reduce__

.. autodata:: pcapkit.corekit.multidict._missing
:no-value:

Type Variables
--------------

Expand Down
41 changes: 41 additions & 0 deletions pcapkit/corekit/multidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import copy
from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload

from pcapkit.utilities.compat import final
from pcapkit.utilities.exceptions import MissingKeyError, UnsupportedCall

if TYPE_CHECKING:
Expand Down Expand Up @@ -75,14 +76,54 @@ def unlink(self, omd: 'OrderedMultiDict') -> 'None':
omd._last_bucket = self.prev # pylint: disable=protected-access


@final
class _Missing:
"""Marker for a ``default`` argument that the caller did not supply.

:meth:`MultiDict.pop` and :meth:`OrderedMultiDict.pop` cannot use
:obj:`None` here, because ``pop(key, None)`` is the canonical :obj:`dict`
idiom for *return* :obj:`None` *rather than raise* -- were the marker
:obj:`None`, that call would raise
:exc:`~pcapkit.utilities.exceptions.MissingKeyError` instead. Both
implementations therefore test this marker by **identity**, never by
truthiness.

Follows :class:`~pcapkit.corekit.fields.field.NoValueType`, the package's
convention for a sentinel of this kind: :func:`~typing.final`, and falsy.

"""

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

def __bool__(self) -> 'Literal[False]':
"""Return :obj:`False`.

A marker meaning *no value was supplied* that answered :obj:`True`
would state the opposite of what it means. The marker is reachable
without touching a private name -- it is the runtime default of both
``pop()`` methods, so :func:`inspect.signature` exposes it -- which is
why this is defined rather than left to :class:`object`.

"""
return False

def __reduce__(self) -> 'str':
"""Return ``'_missing'``, pickling the singleton by name.

Returning a :obj:`str` from :meth:`~object.__reduce__` asks :mod:`pickle`
to save a reference to that global rather than the instance's state, so
unpickling re-resolves :data:`pcapkit.corekit.multidict._missing` and
identity survives the round trip -- which is what the identity tests in
``pop()`` need if the marker ever crosses a process boundary.

"""
return "_missing"


#: _Missing: Marker for an unsupplied ``default`` argument to
#: :meth:`MultiDict.pop` and :meth:`OrderedMultiDict.pop`.
_missing = _Missing()

###############################################################################
Expand Down
73 changes: 73 additions & 0 deletions tests/corekit/test_multidict.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import copy
import inspect
import pickle
import sys
import unittest

from tests._support import bootstrap_core_modules, purge_modules
Expand All @@ -10,6 +13,7 @@ class MultiDictTests(unittest.TestCase):
def setUp(self) -> None:
purge_modules(['pcapkit'])
modules = bootstrap_core_modules()
self.modules = modules
self.multidict = modules['multidict']
self.exceptions = modules['exceptions']

Expand Down Expand Up @@ -100,6 +104,75 @@ def test_multidict_pop_missing_key_raises_custom_error(self) -> None:
with self.assertRaises(self.exceptions.MissingKeyError):
data.pop('missing')

def test_missing_sentinel_follows_novalue_convention(self) -> None:
"""``_missing`` is falsy, like :class:`~pcapkit.corekit.fields.field.NoValueType`.

The sentinel is the runtime default of :meth:`MultiDict.pop` and
:meth:`OrderedMultiDict.pop`, so it is reachable through
:func:`inspect.signature` even though the name is private. A marker
meaning *no default was supplied* that answers :data:`True` to
:func:`bool` says the opposite of what it means, which is why
``NoValueType`` defines ``__bool__``.

Both ``pop()`` implementations decide on **identity** rather than on
truthiness, so the falsy sentinel must not change what they do -- the
falsy-``default`` cases below are the regression guard for that, since
an ``if default:`` in place of ``if default is not _missing:`` would
turn every one of them into a :exc:`MissingKeyError`.

"""
missing = self.multidict._missing

self.assertIs(bool(missing), False)
self.assertFalse(missing)

# unchanged by the above -- ``__reduce__`` returning a bare name is
# pickle-by-name, and it is the only thing that lets the singleton
# survive a round trip through another process
self.assertEqual(repr(missing), 'no value')
self.assertEqual(missing.__reduce__(), '_missing')
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=protocol):
self.assertIs(pickle.loads(pickle.dumps(missing, protocol=protocol)), missing)

for factory in (self.multidict.MultiDict, self.multidict.OrderedMultiDict):
with self.subTest(factory=factory.__name__):
self.assertIs(
inspect.signature(factory.pop).parameters['default'].default, missing
)

with self.assertRaises(self.exceptions.MissingKeyError):
factory().pop('absent')

for default in (None, False, 0, '', [], missing.__class__()):
with self.subTest(default=default):
self.assertIs(factory().pop('absent', default), default)

def test_missing_sentinel_is_marked_final(self) -> None:
"""``_Missing`` carries ``@final``, like ``NoValueType``.

``typing.final`` only started recording ``__final__`` on the decorated
class in Python 3.11, and :data:`pcapkit.utilities.compat.final` is
``typing.final`` on every version from 3.8 up -- so asserting
``__final__`` directly would pass vacuously on 3.8 through 3.10, which
are in the supported range. Probe the decorator with a throwaway class
and skip where it cannot record the mark, rather than assert nothing.

"""
compat = self.modules['compat']

@compat.final
class _Probe:
pass

if getattr(_Probe, '__final__', None) is not True:
self.skipTest(
f'{compat.final!r} does not record __final__ on Python '
f'{".".join(str(part) for part in sys.version_info[:2])}'
)

self.assertIs(self.multidict._Missing.__final__, True)

def test_ordered_multidict_keeps_insertion_order_for_duplicates(self) -> None:
data = self.multidict.OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)])

Expand Down
Loading