Source code for pcapkit.corekit.sentinels

# -*- coding: utf-8 -*-
"""Sentinel Objects
=====================

.. module:: pcapkit.corekit.sentinels

:mod:`pcapkit.corekit.sentinels` is the single, shared home for every
module-level singleton sentinel this package defines for itself -- a value
whose only job is to be recognised by identity (``value is SENTINEL``), so
that it can never be confused with a value a caller might legitimately pass.
See the "Naming a sentinel" section of
:file:`docs/source/contributing/conventions/sentinel-convention.rst` for the
house rule the four below follow.

Before this module existed, each of the four lived beside the one class that
used it: :class:`NullType` in :mod:`pcapkit.corekit.module`,
:class:`NoValueType` in :mod:`pcapkit.corekit.fields.field`,
:class:`NoDefaultType` in :mod:`pcapkit.corekit.enum` and
:class:`AbsentType` in :mod:`pcapkit.protocols.protocol`. The owner's ruling
on GitHub issue #911, verbatim -- *"Okay one module for all four it is."* --
moves the four *definitions* here; each original module keeps a three-line
re-export so that no existing ``from <module> import <name>`` breaks,
including the ``if TYPE_CHECKING:``-only imports of the *types* that
:mod:`pcapkit.foundation.registry.foundation`,
:mod:`pcapkit.foundation.registry.protocols` and
:mod:`pcapkit.corekit.fields.ipaddress`, :mod:`~pcapkit.corekit.fields.misc`,
:mod:`~pcapkit.corekit.fields.numbers` and :mod:`~pcapkit.corekit.fields.strings`
already carry.

``NO_VALUE`` and ``ABSENT`` differ in how far the ruling reaches. ``NO_VALUE``
is documented as the value of
:attr:`FieldBase.default <pcapkit.corekit.fields.field.FieldBase.default>`,
so it is a published contract and the re-export at
:mod:`pcapkit.corekit.fields.field` is load-bearing for callers outside this
package. ``ABSENT`` is private to :mod:`pcapkit.protocols.protocol` --
nothing outside that module ever imports it, from here or from there -- so
its re-export exists only so that module's own code keeps reading
``ABSENT`` rather than a fully-qualified name; see :class:`AbsentType`'s
own docstring below for why it stays private after the move. GitHub issue
#937 later dropped the leading underscore both used to carry (``_Absent``,
``_AbsentType``) in favour of SCREAMING_SNAKE/CamelCase like their two
siblings; the privacy this paragraph describes did not move with the name --
see :class:`AbsentType`'s docstring for what carries it now.

"""
from typing import TYPE_CHECKING

from pcapkit.utilities.compat import final

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

if TYPE_CHECKING:
    from typing import Any, Callable

    from typing_extensions import Literal


[docs] @final class NullType: """Type of :data:`NULL`, the omitted-``class_``/``module`` sentinel. A distinct class rather than a plain :class:`str` -- which is what the registry helpers in :mod:`pcapkit.foundation.registry.protocols` and :mod:`pcapkit.foundation.registry.foundation` used to define, independently of each other -- so that ``is`` comparisons against it mean what they say: no :class:`str` a caller passes, including one that happens to spell ``'(null)'`` itself, can compare equal to this sentinel by identity. See GitHub issue #833. Genuinely a singleton, not merely a class this module happens to instantiate once: :meth:`__new__` always hands back the one instance that already exists, rather than building a new one, so no caller -- direct, or :mod:`copy`/:mod:`pickle` reconstructing an instance behind the scenes -- can end up holding a second object that fails an ``is NULL`` check downstream. :meth:`ModuleDescriptor.klass <pcapkit.corekit.module.ModuleDescriptor.klass>` makes exactly that check, and a stricter guard that raises on a second call would be truer to "singleton" in the abstract, but it would also mean the module's own ``NULL = NullType()`` below is the only call that is ever allowed to succeed -- fragile for no real benefit, since nothing here needs *rejecting* a second construction, only preventing it from producing a distinct object. That still leaves :func:`copy.deepcopy`, :func:`copy.copy` and :mod:`pickle` unhandled: none of them constructs a new instance by calling ``NullType()`` themselves, so the guard above never runs for them. Each is therefore given its own override below, rather than left to fall back to the default behaviour for a plain object: * :func:`copy.copy` and :func:`copy.deepcopy` check for :meth:`__copy__`/:meth:`__deepcopy__` before ever falling back to reduction, so :meth:`__deepcopy__` in particular has to be defined -- its absence is the actual defect this class used to have: deepcopying a :class:`~pcapkit.corekit.module.ModuleDescriptor` recursed into this sentinel, reduced it, and rebuilt a second, non-identical :class:`NullType` that then read as an ordinary attribute name to :func:`getattr`, downgrading a clean :exc:`~pcapkit.utilities.exceptions.ProtocolError` into a bare :exc:`TypeError` (``attribute name must be string, not 'NullType'``). * :mod:`pickle` protocols 2 and up reconstruct through ``cls.__new__(cls)``, which the guarded :meth:`__new__` already keeps to one instance -- but protocols 0 and 1 reconstruct through :func:`copyreg._reconstructor`, which calls :func:`object.__new__` *directly*, bypassing :meth:`__new__` entirely. :meth:`__reduce__` is defined so that every protocol, not only the ones that happen to go through this class's own :meth:`__new__`, is routed through the same module-level getter instead of through reconstruction at all. A caveat rather than a defect: :func:`importlib.reload` on this module re-executes ``NULL = NullType()`` below, producing a *second* singleton that the reloaded code compares against correctly but that every module which already imported the pre-reload :data:`NULL` still holds -- so a comparison spanning the reload sees two "singletons" that are not each other. :meth:`ModuleDescriptor.klass <pcapkit.corekit.module.ModuleDescriptor.klass>` faces exactly this class of problem for the *class* it resolves, which is why it re-reads :data:`sys.modules` on every call rather than memoising; nothing equivalent is possible here, because unlike a resolved class there is no live registry this sentinel could be re-read from. The pre-#833 ``str`` sentinel had the same fragility for the same reason -- it is a property of sharing one module-level binding across a reload, not something this class's singleton guarantees claim to solve -- and nothing in this package reloads :mod:`pcapkit.corekit.sentinels` after import. A second caveat, specific to this class now living apart from its one caller-visible re-export: reloading :mod:`pcapkit.corekit.module` itself -- rather than this module -- no longer has any effect on the singleton at all. That module now only re-imports :data:`NULL` and :class:`NullType` from here, and re-running an already-satisfied ``from ... import`` reads the current binding in :mod:`sys.modules` rather than re-executing anything, so it neither mints a new instance nor loses the old one. The reload hazard this docstring describes moved with the class definition; it did not double. """ #: 'NullType | None': The one instance :meth:`__new__` ever returns, #: including for the module-level ``NULL = NullType()`` below that #: creates it in the first place. Kept on the class rather than as a #: module global so :meth:`__new__` can read and write it without a #: ``global`` statement. _instance: 'NullType | None' = None def __new__(cls) -> 'NullType': """Return the one instance of this class there will ever be.""" if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __bool__(self) -> 'Literal[False]': """Return :obj:`False`.""" return False def __repr__(self) -> 'str': """Return :obj:`str` representation of the sentinel.""" return '<NULL>' def __copy__(self) -> 'NullType': """Return ``self`` -- there is, and only ever will be, one of these.""" return self def __deepcopy__(self, memo: 'dict[int, Any]') -> 'NullType': """Return ``self``, for the same reason as :meth:`__copy__`. Args: memo: The :func:`copy.deepcopy` memo table. Unused: returning ``self`` needs no entry, since nothing about this object is ever copied. """ return self def __reduce__(self) -> 'tuple[Callable[[], NullType], tuple[()]]': """Reduce to the module-level singleton getter, for every :mod:`pickle` protocol. A class that defines :meth:`__reduce__` has it honoured by :meth:`object.__reduce_ex__` for every protocol uniformly, rather than only for the ones that would otherwise call :func:`copyreg._reconstructor` -- so naming :func:`_get_null` here sidesteps reconstruction, and therefore :meth:`__new__`, altogether. That makes this correct independent of whatever :meth:`__new__` does, which is what actually covers protocols 0 and 1; see the class docstring. """ return (_get_null, ())
#: NullType: Sentinel for an omitted ``class_`` argument to the ``register_*`` #: helpers in :mod:`pcapkit.foundation.registry.protocols` and #: :mod:`pcapkit.foundation.registry.foundation`. Housed here, alongside the #: package's other sentinels, rather than in :mod:`pcapkit.corekit.module` #: where it used to live -- per the owner's ruling on GitHub issue #911, see #: the module docstring above. :mod:`pcapkit.corekit.module` keeps a #: re-export so every existing ``from pcapkit.corekit.module import NULL`` #: keeps working. NULL = NullType() def _get_null() -> 'NullType': """Return :data:`NULL`, for :meth:`NullType.__reduce__`. A module-level function rather than a lambda or a bound method, so every :mod:`pickle` protocol -- including 0 and 1, which cannot reference anything nested inside a class -- can name it. """ return NULL
[docs] @final class NoValueType: """Type of :data:`NO_VALUE`, the default value for :mod:`pcapkit.corekit.fields`. Housed here per GitHub issue #911 rather than in :mod:`pcapkit.corekit.fields.field`, where it used to be defined and where :attr:`FieldBase.default <pcapkit.corekit.fields.field.FieldBase.default>` still documents it as the field-default sentinel. """ def __bool__(self) -> 'Literal[False]': """Return :obj:`False`.""" return False
#: NoValueType: Default value for #: :attr:`FieldBase.default <pcapkit.corekit.fields.field.FieldBase.default>`. #: :mod:`pcapkit.corekit.fields.field` keeps a re-export, since that #: attribute's own documentation is a published contract naming this object. #: Renamed from ``NoValue`` to ``NO_VALUE`` by GitHub issue #937, which #: normalised all four sentinel *objects* to SCREAMING_SNAKE. NO_VALUE = NoValueType()
[docs] @final class NoDefaultType: """Type of :data:`NO_DEFAULT`, the omitted-``default`` sentinel for :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>`. A dedicated class rather than a bare :class:`object`, per the owner's ruling on #859: *"use dedicated class rather than bare object. Follow the house convention."* A bare :class:`object` compares under ``is`` exactly as safely as a dedicated class with no ``__eq__`` of its own does -- identity comparison was never the problem an earlier revision's docstring here overstated it to be. What a bare :class:`object` actually lacks is a readable representation: it prints as ``<object object at 0x...>`` in a signature, in :func:`help`, and in a traceback, where ``NoDefaultType()`` -- via :meth:`__repr__` below -- prints as ``<NO_DEFAULT>``. Named ``NoDefaultType`` for the *class* because that half of the house convention is settled: both :class:`NullType` and :class:`NoValueType` use ``<Name>Type``. At the time, the *instance*'s own name was not similarly settled -- the owner's follow-up on #859 was explicit that ``NULL`` (``SCREAMING_CASE``) and ``NoValue`` (``CapWords``) disagreed, and "mainly depends on how we need it." The need here was continuity: ``NO_DEFAULT`` was already the name on ``main`` -- referenced in :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>`'s signature, its docstring, and both comparison sites -- and that change was to *what the sentinel is*, not to *what it is called*, so it kept that name rather than being renamed to match either precedent's instance casing for its own sake. ``NULL``'s ``SCREAMING_CASE`` was the closer match regardless, since :data:`NO_DEFAULT` was already spelled that way -- and GitHub issue #937 later settled the question this paragraph left open: ``NoValue`` became :data:`NO_VALUE` and ``_Absent`` became :data:`ABSENT`, so every instance name now agrees on SCREAMING_SNAKE. Genuinely a singleton, not merely a class this module happens to instantiate once: :meth:`__new__` always hands back the one instance that already exists, rather than building a new one. That guards against a caller writing ``registry.get(key, default=NoDefaultType())`` -- perhaps not realising :data:`NO_DEFAULT` already exists -- and getting back a *second*, non-identical sentinel that silently fails ``is NO_DEFAULT`` inside :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>`, so their call is treated as supplying a real (if useless) default instead of the *no default* they meant. With the guard, :class:`NoDefaultType() <NoDefaultType>` always returns the one canonical :data:`NO_DEFAULT`, so that mistake self-corrects. Unlike :class:`NullType`, this does *not* also define ``__copy__``, ``__deepcopy__`` or ``__reduce__`` -- but not because :func:`copy.deepcopy` or :mod:`pickle` "bypass" :meth:`__new__`; they do not, for protocol 2 and above. :meth:`object.__reduce_ex__` at protocol 2 reduces through :func:`copyreg.__newobj__`, which reconstructs by calling ``cls.__new__(cls)`` -- exactly the guarded path above -- so ``copy.copy``, ``copy.deepcopy`` and every pickle protocol from 2 on already come back as the one canonical instance with no extra code. Measured:: >>> NO_DEFAULT.__reduce_ex__(2) (<function __newobj__ at 0x...>, (<class '...NoDefaultType'>,), None, None, None) >>> copy.deepcopy(NO_DEFAULT) is NO_DEFAULT True The one path :meth:`__new__` cannot see is pickle protocol 0 (and 1), which reduces through :func:`copyreg._reconstructor` instead, and *that* calls :func:`object.__new__` directly:: >>> NO_DEFAULT.__reduce_ex__(0) (<function _reconstructor at 0x...>, (<class '...NoDefaultType'>, <class 'object'>, None)) >>> pickle.loads(pickle.dumps(NO_DEFAULT, protocol=0)) is NO_DEFAULT False That gap is exactly what :class:`NullType`'s own :meth:`~NullType.__reduce__` exists to close, because :data:`NULL` is stored as a :class:`~pcapkit.corekit.module.ModuleDescriptor` field that a caller's own :func:`copy.deepcopy` or :mod:`pickle` call can walk into and reconstruct. :data:`NO_DEFAULT` is left unhandled here not because the gap cannot occur in principle, but because nothing in this package ever pickles it at protocol 0: it is reachable -- from :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>`'s own bound parameter default (``EnumLookup.get.__defaults__[0]``, or any subclass's, e.g. ``Hardware.get.__func__.__defaults__[0]``) and from ``inspect.signature(Hardware.get).parameters['default'].default`` -- but neither is a field any object here gets pickled *as*, and both still survive :func:`copy.deepcopy` with identity intact regardless, because deep-copying either still reduces the sentinel itself through the same, guarded protocol-2 path measured above. A caveat that turns out to be dormant rather than live, on the current tree -- worth stating precisely rather than either repeating the older, inaccurate claim or dropping the topic. :func:`importlib.reload` on this module re-executes both ``class NoDefaultType:`` and ``NO_DEFAULT = NoDefaultType()`` below, producing a fresh, distinct object; any consumer that had already captured the pre-reload one -- as a bound parameter default, say -- goes on holding the stale one, and a bare ``held_default is NO_DEFAULT`` comparison against the post-reload global then reads :data:`False` where it once read :data:`True`. :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>` looked exactly like such a consumer before GitHub issue #864: an unrecognised, non-``NO_DEFAULT`` value used to fall through to ``cls(default)``, so a stale sentinel handed to that call could raise a :exc:`ValueError` a caller had no reason to expect from an *omitted* argument. #864 closed a different hole -- ``default`` could mint a new member -- by replacing that call with a ``default not in cls._value2member_map_`` guard, and the guard happens to close this one too: a :class:`NoDefaultType` instance, stale or fresh, is never a registered enum value, so the guard's ``not in`` half reads :data:`True` for it either way and ``get`` re-raises the original lookup error correctly regardless of which :data:`NO_DEFAULT` a caller's stale default is stale *against*. Measured on the current tree, guard included:: >>> Hardware.get('Definitely-Not-A-Member') # before reload KeyError: 'Definitely-Not-A-Member' >>> importlib.reload(pcapkit.corekit.sentinels) >>> Hardware.get('Definitely-Not-A-Member') # after reload KeyError: 'Definitely-Not-A-Member' So the docstring this class carried before GitHub issue #911's move -- which claimed the second call above raises :exc:`ValueError` -- was already wrong on ``main`` at ``d31c0aaf6``, independently of the move: it described the pre-#864 ``cls(default)`` call, and nobody had re-verified it against the guard #864 added afterwards. Fixed here as a drive-by correction, not a consequence of the housing change itself. None of that makes the underlying hazard theoretical elsewhere in this package: ``-1`` never had this failure mode at all, since ``-1 == -1`` compares by value rather than identity, and reload staleness is a *tracked* defect class here for other constructs -- see :meth:`pcapkit.protocols.protocol.ProtocolBase._lookup_next_layer`'s own docstring note citing GitHub issues #425, #428 and #560, and :mod:`tests.protocols.test_dispatch_default_resolution_unit`'s own ``test_no_stale_class_survives_a_module_reload``, which reloads a module deliberately to pin the fix for exactly that class of bug elsewhere. A *future* comparison site written the vulnerable way -- a bare ``is NO_DEFAULT`` with no independent guard behind it, the way #864's fix itself was not -- would still reproduce it. "Nothing in this package reloads :mod:`pcapkit.corekit.sentinels` after import" remains true today, but it is a caveat to keep honest rather than a guarantee this class enforces. .. note:: GitHub issue #911 also changes *which* reload is the one that matters, independently of the #864 finding above. :mod:`pcapkit.corekit.enum` no longer defines ``NoDefaultType`` itself; it only reads :data:`NO_DEFAULT` off this module once, at its own import time, into its own module global. Reloading :mod:`pcapkit.corekit.enum` alone therefore now just re-runs that read, which -- so long as this module has not *also* been reloaded -- fetches back the identical object and changes nothing. Producing a fresh :data:`NO_DEFAULT` at all now takes reloading *this* module, where the class statement lives; reloading only :mod:`pcapkit.corekit.enum` is not enough on its own, because ``from ... import`` binds a copy rather than a live alias, and that module's own global stays pointed at whatever it read until something re-runs that import. Also unlike both :class:`NullType` and :class:`NoValueType`, this deliberately does *not* define ``__bool__``. Both of those model an *absent* value, so reading falsy in a boolean context is the point. :data:`NO_DEFAULT` models something different: a marker meaning *no default was supplied*, checked exclusively by ``is`` at :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>`'s two comparison sites -- nothing here ever evaluates it for truthiness. Giving it ``__bool__ -> False`` for symmetry with the other two would invite exactly the conflation this sentinel exists to rule out: code that writes ``if not default:`` instead of ``if default is NO_DEFAULT:`` would then read :data:`NO_DEFAULT` the same way it reads a caller's genuine falsy default -- ``0``, ``''``, ``None`` or ``False`` -- which is the exact collision ``-1`` used to cause under ``==`` and the reason #857 exists. Leaving ``__bool__`` undefined makes ``NoDefaultType()`` truthy (the default for any object defining neither ``__bool__`` nor ``__len__``), which at least does not *look* like one of the falsy values it must never be mistaken for. """ #: 'NoDefaultType | None': The one instance :meth:`__new__` ever returns, #: including for the module-level ``NO_DEFAULT = NoDefaultType()`` below #: that creates it in the first place. Kept on the class rather than as a #: module global so :meth:`__new__` can read and write it without a #: ``global`` statement. _instance: 'NoDefaultType | None' = None def __new__(cls) -> 'NoDefaultType': """Return the one instance of this class there will ever be.""" if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __repr__(self) -> 'str': """Return :obj:`str` representation of the sentinel.""" return '<NO_DEFAULT>'
#: NoDefaultType: The ``default`` argument value that means *no default*, i.e. #: let an unresolvable key propagate its lookup error rather than falling #: back. See :class:`NoDefaultType` for why this is a dedicated class rather #: than a bare :class:`object`, why it is a guarded singleton, and why it #: defines neither the copy/pickle hooks nor the ``__bool__`` that its #: :mod:`pcapkit.corekit` siblings do. :mod:`pcapkit.corekit.enum` keeps a #: re-export, since :meth:`EnumLookup.get <pcapkit.corekit.enum.EnumLookup.get>` #: names this object in its signature and docstring. NO_DEFAULT = NoDefaultType()
[docs] @final class AbsentType: """Type of :data:`ABSENT`, the absent-key sentinel. A distinct class rather than a bare :obj:`object` so that the sentinel has a name of its own in a traceback or a debugger, and so that a type checker has something to name where ``object()`` would give it nothing. It follows :class:`NoValueType`, which does the same job for an unset field default; this is a sibling of it rather than a reuse, since that one is documented as the default value of :attr:`FieldBase.default <pcapkit.corekit.fields.field.FieldBase.default>` and means "no value was given", not "this key is not here". Defined here, alongside the package's other sentinels, per the owner's ruling on GitHub issue #911. Originally named ``_AbsentType``/``_Absent``, with the leading underscore standing in for "private" -- GitHub issue #937 normalised every sentinel *object* to SCREAMING_SNAKE and dropped it, so this pair now reads as CamelCase/SCREAMING_SNAKE like their two siblings and privacy is no longer signalled by the name at all. The owner's ruling on #937, verbatim: *"we can change* ``_ABSENT`` *to* ``ABSENT`` *just document it as private type/class in the documentation and not for public use is enough."* So this class and :data:`ABSENT` stay exactly as private as they were: nothing outside :mod:`pcapkit.protocols.protocol` reads :data:`ABSENT`, from here or from there, and neither this module's nor that module's :attr:`__all__` names either one. This docstring, and the "Naming a sentinel" section of :file:`docs/source/contributing/conventions/sentinel-convention.rst`, are what now records that fact in place of the leading underscore. """ def __bool__(self) -> 'Literal[False]': """Return :obj:`False`.""" return False def __repr__(self) -> 'str': """Return :obj:`str` representation of the sentinel.""" return '<absent>'
#: AbsentType: Absent-versus-:obj:`None` sentinel for #: :func:`pcapkit.protocols.protocol._declared_keywords` to read a class's #: own ``__keywords__`` out of its :attr:`~object.__dict__`, where #: :obj:`None` is itself a meaningful value -- the opt-out that says the #: class cannot enumerate its keywords, c.f. #: :attr:`ProtocolBase.__keywords__ #: <pcapkit.protocols.protocol.ProtocolBase.__keywords__>`. Never leaves #: :mod:`pcapkit.protocols.protocol`, which keeps a private re-export of it #: for exactly that one read. Private by convention and documentation only, #: not by a leading underscore -- see :class:`AbsentType`'s own docstring for #: why, per GitHub issue #937. ABSENT = AbsentType()