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
77 changes: 47 additions & 30 deletions docs/source/contributing/conventions/sentinel-convention.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ What reaches users is the **object only**. The owner ruled on GitHub issue :issu
that the objects alone -- ``NULL`` and its siblings -- are exported to users, so a
public sentinel names its instance in :mod:`pcapkit.corekit.sentinels`'s ``__all__`` and
leaves the type out of it. The type stays importable by its dotted path there, for an
annotation or an ``is`` guard; it is only ``import *`` that does not offer it. A private
annotation or an ``is`` guard; it is only ``import *`` that does not offer it. The
``__all__`` there does name :class:`~pcapkit.corekit.sentinels.Sentinel`, the abstract
base every sentinel type shares, since that is no sentinel's own type. A private
sentinel such as ``ABSENT`` is in neither, which is what private means here -- dropping
its leading underscore did not add it to either list, and
:class:`~pcapkit.corekit.sentinels.AbsentType` and
Expand All @@ -69,12 +71,13 @@ public use rather than left off it, since the name alone no longer says so.

.. note::

Of the sentinels, only :class:`~pcapkit.corekit.sentinels.NullType` is a full worked
example. :class:`~pcapkit.corekit.sentinels.NoValueType` follows the naming rule but
is **not** a singleton (``NoValueType() is NO_VALUE`` is :obj:`False`) and has no
``__repr__`` of its own, so it demonstrates the name and nothing else; ``AbsentType``
has a ``__repr__`` (``<absent>``) but no singleton guard either. Copy ``NullType``
when you need a pattern to follow.
Every sentinel type subclasses :class:`~pcapkit.corekit.sentinels.Sentinel`, the public
abstract base that carries the singleton protocol, the repr and the immutability, and
whose metaclass is :class:`~pcapkit.corekit.sentinels.SentinelMeta`. So a new sentinel
is short: subclass it, set ``__slots__ = ()`` and ``_name`` to the object's name, and
add ``__bool__`` if the sentinel stands for an absent value.
:class:`~pcapkit.corekit.sentinels.NoValueType` is the smallest example. Everything
goes in the ``class`` body, since the type refuses any attribute set on it afterwards.

Why a Class and Not ``object()``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -90,7 +93,7 @@ appears in a signature, in :func:`help` output and in a traceback. Compare what
default: 'Any' = <object object at 0x7fc393324cf0>

# dedicated type with __repr__
default: 'Any' = <NO_DEFAULT>
default: 'Any' = NO_DEFAULT

.. warning::

Expand All @@ -110,14 +113,15 @@ ever sees. The rule above is for sentinels this package writes itself.
Per-Sentinel Dunder Methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~

The sentinels deliberately differ, and the differences are **needs, not
inconsistencies**:
Most of the protocol is shared and lives on ``Sentinel``. Where the sentinels differ,
the differences are **needs, not inconsistencies**:

``__new__`` returning a cached instance
Guards against a caller constructing a second, non-identical sentinel that then
fails every ``is`` check. Worth having wherever the type is reachable by a caller at
all -- which, since the type is kept out of ``__all__``, means wherever it is
importable by its dotted path rather than wherever it is star-exported.
Every sentinel has it, from the base, and returns the instance the metaclass made as
the ``class`` statement ran. It guards against a caller constructing a second,
non-identical sentinel that then fails every ``is`` check, and it hands back the
cached instance rather than raising, as ``type(None)()`` does, so that mistake
corrects itself.
:class:`~pcapkit.corekit.sentinels.NullType` documents the limit honestly: a module
**reload** re-executes the class statement, so the guard does not survive one, and
code holding the pre-reload instance will fail ``is``. The module that matters there
Expand All @@ -134,20 +138,33 @@ inconsistencies**:
prevent.

``__copy__`` / ``__deepcopy__`` / ``__reduce__``
:class:`~pcapkit.corekit.sentinels.NullType` is the only sentinel that has
them, because ``NULL`` is stored in a
:class:`~pcapkit.corekit.module.ModuleDescriptor` field, so a caller's
:func:`copy.deepcopy` or :mod:`pickle` can walk into it and would otherwise
reconstruct a second instance. ``NO_VALUE``, ``NO_DEFAULT`` and ``ABSENT`` have none.
Measured, since the consequence differs: ``NO_DEFAULT`` survives a copy identically
anyway, because its ``__new__`` hands back the cached instance, while
``copy.copy(NO_VALUE)`` and ``copy.copy(ABSENT)`` each build a distinct object that
fails both ``is`` and ``==``. Neither is reached by a copy that would notice:
``ABSENT`` never leaves the
module that reads it, and ``NO_VALUE``, though it rides
:attr:`FieldBase.default <pcapkit.corekit.fields.field.FieldBase.default>` through a
field copy once per field per packet, keeps its identity there because
:meth:`FieldBase.__copy__ <pcapkit.corekit.fields.field.FieldBase.__copy__>` is
shallow and shares the reference. Add them all when, and only when, the sentinel
becomes reachable from a copy that is not.
Every sentinel has them, from the base. The owner ruled on :issue:`1842` that every
sentinel is a true singleton, rather than gaining these only once a deep copy is
found to reach it -- as one had by then, through a parsed schema and a field that
hold ``NO_VALUE``. ``__copy__`` and ``__deepcopy__`` return the object itself, and
``__reduce__`` returns its module-level name, so :mod:`pickle` stores a reference to
that name and every protocol loads the same object back. A guarded ``__new__``
alone would not do, since pickle protocols 0 and 1 rebuild through
:func:`copyreg._reconstructor`, which calls :meth:`object.__new__` directly. That
path also limits pickles written before :issue:`1842`: one of ``NO_VALUE``,
``NO_DEFAULT`` or ``ABSENT`` at protocol 0 or 1 still loads, but as a second
instance, because nothing on the class can intercept it.

``__repr__``
Every sentinel has it, from the base. It is the object's own name -- ``NULL``,
``NO_VALUE``, ``NO_DEFAULT``, ``ABSENT`` -- as ``repr(None)`` is ``'None'``, and
:func:`str` gives the same. That is what a signature, :func:`help` and a traceback
show.

``__setattr__`` / ``__delattr__``
Every sentinel has them, from the base, and they raise :exc:`AttributeError` for any
name, as ``None.x = 1`` does. ``__slots__`` is empty, so there is no ``__dict__`` to
reach round them either. The metaclass does the same for the type, raising
:exc:`TypeError` as ``type(None).x = 1`` does, so neither ``_name`` nor the cached
instance can be swapped out. It also refuses to let a concrete sentinel type be
subclassed, by a ``class`` statement or by ``register``, as ``type(None)`` cannot be;
``Sentinel`` itself stays subclassable, being abstract. The owner asked for this in
review of the change for :issue:`1842`: a sentinel's repr and its other behaviour
follow Python's own singletons, :obj:`None`, :obj:`Ellipsis` and
:obj:`NotImplemented`.

18 changes: 16 additions & 2 deletions docs/source/pcapkit/corekit/sentinels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@ caller might legitimately pass.
They are all defined here, and this is their only import path: the modules
that use them do not re-export them (:issue:`1744`).

Each behaves like a built-in singleton such as :obj:`None`: calling its type,
copying it and pickling it give back the one instance, it reprs as its own
name, neither it nor its type can be changed, and its type cannot be
subclassed (:issue:`1842`).

.. versionadded:: 1.5.0

:class:`NoValueType` moves here from :mod:`pcapkit.corekit.fields.field`
(:issue:`911`), and :data:`NO_VALUE` replaces its ``NoValue`` there
(:issue:`937`).
(:issue:`937`). :class:`Sentinel` is the public base of every sentinel
type (:issue:`1842`).

.. autoclass:: pcapkit.corekit.sentinels.Sentinel

.. autoclass:: pcapkit.corekit.sentinels.NullType
.. autodata:: pcapkit.corekit.sentinels.NULL

.. autoclass:: pcapkit.corekit.sentinels.NoValueType
.. autodata:: pcapkit.corekit.sentinels.NO_VALUE
:no-value:

.. autoclass:: pcapkit.corekit.sentinels.NoDefaultType
.. autodata:: pcapkit.corekit.sentinels.NO_DEFAULT
Expand All @@ -37,3 +44,10 @@ that use them do not re-export them (:issue:`1744`).

.. autoclass:: pcapkit.corekit.sentinels.AbsentType
.. autodata:: pcapkit.corekit.sentinels.ABSENT

Internal Definitions
--------------------

.. autoclass:: pcapkit.corekit.sentinels.SentinelMeta
:no-members:
:show-inheritance:
Loading
Loading