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
69 changes: 43 additions & 26 deletions docs/source/contributing/conventions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,17 @@ Naming a sentinel

A *sentinel* here is a module-level singleton 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. The house rule, from the maintainer:
caller might legitimately pass. The house rule, from the maintainer, covers the type:

Keep the sentinel object's type class naming as ``<SENTINEL>Type``.

That is, the class takes the instance's name in CamelCase with ``Type`` appended. The
four in the tree follow it:
That is, the class takes the instance's name in CamelCase with ``Type`` appended. It
says nothing about the **object**'s own name, which is what let three casings diverge
with no rule naming any of them wrong. GitHub issue #937 closed that gap, verbatim:
*"take SCREAMING_SNAKE and accept the breaking change (no backport needed)."* So the
object is named in SCREAMING_SNAKE, and the type-naming rule above derives from it
mechanically -- title-case each underscore-separated word and append ``Type``, no
per-sentinel exception needed. The four in the tree follow it:

.. list-table::
:header-rows: 1
Expand All @@ -163,14 +168,14 @@ four in the tree follow it:
* - ``NULL``
- ``NullType``
- :mod:`pcapkit.corekit.sentinels`
* - ``NoValue``
* - ``NO_VALUE``
- ``NoValueType``
- :mod:`pcapkit.corekit.sentinels`
* - ``NO_DEFAULT``
- ``NoDefaultType``
- :mod:`pcapkit.corekit.sentinels`
* - ``_Absent``
- ``_AbsentType``
* - ``ABSENT``
- ``AbsentType``
- :mod:`pcapkit.corekit.sentinels`

All four used to live beside the one class that used them --
Expand All @@ -182,41 +187,53 @@ names; each original module keeps a re-export so every existing
``from <module> import <name>`` keeps working, including the
``if TYPE_CHECKING:``-only imports of the types.

Note what the rule does **not** fix: the **instance** name's casing is deliberately
free, which is why ``NULL`` and ``NoValue`` disagree and both are correct. Pick
whichever reads better at the call site, and where a name already exists, keep it --
renaming a published sentinel costs every caller for no gain.

Nor does it fix the **leading underscore**. ``_Absent`` is private -- it is read in
``_declared_keywords`` and discarded there, never leaving
:mod:`pcapkit.protocols.protocol` even though its *definition* now does -- and it is
still held to the convention, which is why its type is ``_AbsentType`` and not
``_Absent_t`` or ``Absent``. It is a deliberate fourth rather than an accident, and
its own docstring (:file:`pcapkit/corekit/sentinels.py`, line 430) says so:
Before GitHub issue #937, the **instance** name's casing was deliberately free, which is
why ``NULL`` and ``NoValue`` disagreed and both were called correct -- three sentinels
had already picked three different casings (``NULL`` SCREAMING_SNAKE, ``NoValue``
CamelCase, ``_Absent`` CamelCase with a leading underscore) before anyone ruled on it.
#937's ruling closes that: SCREAMING_SNAKE is now the one answer, and the two renames
it made -- ``NoValue`` to ``NO_VALUE``, ``_Absent`` to ``ABSENT`` -- are the breaking
change it accepted rather than deprecating. Where a sentinel name already exists and
already follows SCREAMING_SNAKE, keep it; renaming a published sentinel again costs
every caller for no further gain.

The rename also dropped the **leading underscore** ``_Absent``/``_AbsentType`` used to
carry. ``ABSENT`` is private -- it is read in ``_declared_keywords`` and discarded
there, never leaving :mod:`pcapkit.protocols.protocol` -- and the underscore used to be
the mechanical signal of that. The maintainer'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 privacy is documentation-only from
here on, carried by this paragraph and by :class:`AbsentType`'s own docstring
(:file:`pcapkit/corekit/sentinels.py`, line 439), which still says so:

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 [...]

The private name is also why this table listed three for as long as it did: a sweep
filtered on capitalised names does not see it. When adding a sentinel, add it here
whether or not it is public.
It remains a deliberate fourth rather than an accident: the leading underscore's
absence is also why this table once listed three for as long as it did -- a sweep
filtered on capitalised names did not see ``_Absent`` -- and that history does not
change now that nothing in the name itself marks it out. When adding a sentinel, add
it here whether or not it is public.

What reaches users is the **object only**. The maintainer's ruling: *"we should ONLY
export the objects (like* ``NULL`` *) to users"* -- so a public sentinel names its
instance in its module's ``__all__`` and leaves the type out of it (GitHub issue #911).
The type stays importable by its dotted path, for an annotation or an ``is`` guard; it
is ``import *`` that no longer offers it. A private sentinel such as ``_Absent`` is in
neither, which is what private means here.
is ``import *`` that no longer offers it. 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:`AbsentType` and :data:`ABSENT` are documented on
:doc:`the sentinels API page </pcapkit/corekit/sentinels>` as private and not for
public use rather than left off it, since the name alone no longer says so.

.. note::

Of the four, only :class:`~pcapkit.corekit.sentinels.NullType` is a full worked
example. ``NoValueType`` follows the naming rule but is **not** a singleton
(``NoValueType() is NoValue`` is :obj:`False`) and has no ``__repr__`` of its own,
so it demonstrates the name and nothing else; ``_AbsentType`` has a ``__repr__``
(``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.

Expand Down Expand Up @@ -270,7 +287,7 @@ inconsistencies**:
has any effect on it.

``__bool__`` returning :obj:`False`
``NULL``, ``NoValue`` and ``_Absent`` have it, because each stands for an *absent
``NULL``, ``NO_VALUE`` and ``ABSENT`` have it, because each stands for an *absent
value* and reads naturally in a boolean test. ``NO_DEFAULT`` deliberately does
**not**: it is a marker meaning *no default was supplied*, it is only ever tested
with ``is``, and making it falsy would invite ``if not default:`` -- which would
Expand All @@ -282,7 +299,7 @@ inconsistencies**:
:class:`~pcapkit.corekit.sentinels.NullType` 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_DEFAULT`` and ``_Absent`` have none, because
reconstruct a second instance. ``NO_DEFAULT`` and ``ABSENT`` have none, because
neither is ever stored in any structure a caller copies -- one only ever appears as
a default argument, and the other never leaves the module that reads it.
Add them when, and only when, the sentinel becomes reachable from something
Expand Down
2 changes: 1 addition & 1 deletion docs/source/pcapkit/corekit/fields/field.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Auxiliaries
-----------

.. autoclass:: pcapkit.corekit.fields.field.NoValueType
.. autodata:: pcapkit.corekit.fields.field.NoValue
.. autodata:: pcapkit.corekit.fields.field.NO_VALUE
:no-value:

Internal Definitions
Expand Down
21 changes: 19 additions & 2 deletions docs/source/pcapkit/corekit/sentinels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,33 @@ Each of the three below used to live beside the one class that consumed it --
:class:`NullType` in :mod:`pcapkit.corekit.module`, :class:`NoValueType` in
:mod:`pcapkit.corekit.fields.field` and :class:`NoDefaultType` in
:mod:`pcapkit.corekit.enum` -- until GitHub issue #911 moved all four
definitions here, the private ``_AbsentType``/``_Absent`` included. Each
definitions here, the private ``AbsentType``/``ABSENT`` included. Each
original module keeps a re-export, so every existing ``from <module> import
<name>`` keeps working unchanged.

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

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

.. autoclass:: pcapkit.corekit.sentinels.NoDefaultType
.. autodata:: pcapkit.corekit.sentinels.NO_DEFAULT

.. note::

:class:`AbsentType` and :data:`ABSENT` are **private** -- never imported outside
:mod:`pcapkit.protocols.protocol`, and named in no module's ``__all__``. Until
GitHub issue #937, the leading underscore they carried (``_AbsentType``/
``_Absent``) hid them from Sphinx automatically, the way it hides every other
``_``-prefixed name; dropping the underscore for SCREAMING_SNAKE consistency with
the other three sentinels (see :ref:`sentinel-convention`) means Sphinx would
otherwise document them as though they were public. They are documented below
instead, explicitly marked private, per the maintainer's ruling on #937: *"we can
change* ``_ABSENT`` *to* ``ABSENT`` *just document it as private type/class in the
documentation and not for public use is enough."* Neither is for use outside this
package.

.. autoclass:: pcapkit.corekit.sentinels.AbsentType
.. autodata:: pcapkit.corekit.sentinels.ABSENT
14 changes: 7 additions & 7 deletions pcapkit/corekit/fields/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import struct
from typing import TYPE_CHECKING, Generic, TypeVar, cast

from pcapkit.corekit.sentinels import NoValue, NoValueType # pylint: disable=unused-import
from pcapkit.corekit.sentinels import NO_VALUE, NoValueType # pylint: disable=unused-import
from pcapkit.utilities.exceptions import FieldValueError, NoDefaultValue, ProtocolError

__all__ = ['NoValue', 'Field']
__all__ = ['NO_VALUE', 'Field']

if TYPE_CHECKING:
from typing import IO, Any, Callable, Iterator, Optional
Expand Down Expand Up @@ -248,7 +248,7 @@ class FieldBase(Generic[_T], metaclass=FieldMeta):
# fields takes no default value of its own -- and reading :attr:`default` off
# one of those raised :exc:`AttributeError` for a private attribute rather
# than reporting that the field declares no default. See #422.
_default: '_T | NoValueType' = NoValue
_default: '_T | NoValueType' = NO_VALUE

@property
def name(self) -> 'str':
Expand All @@ -273,7 +273,7 @@ def default(self, value: '_T | NoValueType') -> 'None':
@default.deleter
def default(self) -> 'None':
"""Delete field default value."""
self._default = NoValue
self._default = NO_VALUE

@property
def template(self) -> 'str':
Expand Down Expand Up @@ -347,7 +347,7 @@ def __init__(self, *args: 'Any', **kwargs: 'Any') -> 'None':
if not hasattr(self, '_name'):
self._name = f'<{type(self).__name__[:-5].lower()}>'

self._default = NoValue
self._default = NO_VALUE
self._template = '0s'
self._callback = lambda *_: None

Expand Down Expand Up @@ -428,7 +428,7 @@ def pack(self, value: 'Optional[_T]', packet: 'dict[str, Any]') -> 'bytes':

"""
if value is None:
if self._default is NoValue:
if self._default is NO_VALUE:
raise NoDefaultValue(f'Field {self.name} has no default value.')
value = cast('_T', self._default)

Expand Down Expand Up @@ -609,7 +609,7 @@ def template(self) -> 'str':
return self._template

def __init__(self, length: 'int | Callable[[dict[str, Any]], int]',
default: '_T | NoValueType' = NoValue,
default: '_T | NoValueType' = NO_VALUE,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
#self._name = '<unknown>'
if not hasattr(self, '_name'):
Expand Down
10 changes: 5 additions & 5 deletions pcapkit/corekit/fields/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import ipaddress
from typing import TYPE_CHECKING, Generic, TypeVar, cast

from pcapkit.corekit.fields.field import Field, NoValue
from pcapkit.corekit.fields.field import NO_VALUE, Field
from pcapkit.utilities.exceptions import FieldValueError

__all__ = [
Expand Down Expand Up @@ -310,7 +310,7 @@ def version(self) -> 'Literal[4]':
"""IP version number."""
return 4

def __init__(self, default: 'IPv4Address | NoValueType' = NoValue,
def __init__(self, default: 'IPv4Address | NoValueType' = NO_VALUE,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
super().__init__(4, default, callback)

Expand All @@ -332,7 +332,7 @@ def version(self) -> 'Literal[6]':
"""IP version number."""
return 6

def __init__(self, default: 'IPv6Address | NoValueType' = NoValue,
def __init__(self, default: 'IPv6Address | NoValueType' = NO_VALUE,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
super().__init__(16, default, callback)

Expand Down Expand Up @@ -367,7 +367,7 @@ def version(self) -> 'Literal[4]':
"""IP version number."""
return 4

def __init__(self, default: 'IPv4Interface | NoValueType' = NoValue,
def __init__(self, default: 'IPv4Interface | NoValueType' = NO_VALUE,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
super().__init__(8, default, callback)

Expand Down Expand Up @@ -455,7 +455,7 @@ def version(self) -> 'Literal[6]':
"""IP version number."""
return 6

def __init__(self, default: 'IPv6Interface | NoValueType' = NoValue,
def __init__(self, default: 'IPv6Interface | NoValueType' = NO_VALUE,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
super().__init__(17, default, callback)

Expand Down
22 changes: 11 additions & 11 deletions pcapkit/corekit/fields/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import io
from typing import TYPE_CHECKING, TypeVar, cast

from pcapkit.corekit.fields.field import FieldBase, NoValue
from pcapkit.corekit.fields.field import NO_VALUE, FieldBase
from pcapkit.utilities.exceptions import FieldError, NoDefaultValue
from pcapkit.utilities.warnings import RegistryWarning, warn

Expand Down Expand Up @@ -32,7 +32,7 @@
class NoValueField(FieldBase[_TN]):
"""Schema field for no value type (or :obj:`None`)."""

_default = NoValue
_default = NO_VALUE

@property
def template(self) -> 'str':
Expand Down Expand Up @@ -105,7 +105,7 @@ def default(self, value: '_TC | NoValueType') -> 'None':
@default.deleter
def default(self) -> 'None':
"""Delete field default value."""
self._field.default = NoValue
self._field.default = NO_VALUE

@property
def template(self) -> 'str':
Expand Down Expand Up @@ -303,7 +303,7 @@ def protocol(self, protocol: 'Type[_TP] | str') -> 'None':
self._protocol = protocol

def __init__(self, length: 'int | Callable[[dict[str, Any]], int]' = lambda _: -1,
default: '_TP | NoValueType | bytes' = NoValue,
default: '_TP | NoValueType | bytes' = NO_VALUE,
protocol: 'Optional[Type[_TP] | str]' = None,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
#self._name = '<payload>'
Expand Down Expand Up @@ -363,7 +363,7 @@ def pack(self, value: 'Optional[_TP | Schema | bytes]', packet: 'dict[str, Any]'

"""
if value is None:
if self._default is NoValue:
if self._default is NO_VALUE:
raise NoDefaultValue(f'Field {self.name} has no default value.')
value = cast('_TP', self._default)

Expand Down Expand Up @@ -432,7 +432,7 @@ def default(self, value: '_TC | NoValueType') -> 'None':
@default.deleter
def default(self) -> 'None':
"""Delete field default value."""
self._field.default = NoValue
self._field.default = NO_VALUE

@property
def template(self) -> 'str':
Expand Down Expand Up @@ -489,7 +489,7 @@ def pre_process(self, value: '_TC', packet: 'dict[str, Any]') -> 'Any': # pylin

"""
if self._field is None:
return NoValue # type: ignore[unreachable]
return NO_VALUE # type: ignore[unreachable]
return self._field.pre_process(value, packet)

def pack(self, value: 'Optional[_TC]', packet: 'dict[str, Any]') -> 'bytes':
Expand Down Expand Up @@ -519,7 +519,7 @@ def post_process(self, value: 'Any', packet: 'dict[str, Any]') -> '_TC': # pyli

"""
if self._field is None:
return NoValue # type: ignore[unreachable]
return NO_VALUE # type: ignore[unreachable]
return self._field.post_process(value, packet)

def unpack(self, buffer: 'bytes | IO[bytes]', packet: 'dict[str, Any]') -> '_TC':
Expand Down Expand Up @@ -658,7 +658,7 @@ def schema(self) -> 'Type[_TS]':

def __init__(self, length: 'int | Callable[[dict[str, Any]], int]' = lambda _: -1,
schema: 'Optional[Type[_TS]]' = None,
default: '_TS | NoValueType | bytes' = NoValue,
default: '_TS | NoValueType | bytes' = NO_VALUE,
packet: 'Optional[dict[str, Any]]' = None,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
#self._name = '<schema>'
Expand Down Expand Up @@ -720,7 +720,7 @@ def pack(self, value: 'Optional[_TS | bytes]', packet: 'dict[str, Any]') -> 'byt

"""
if value is None:
if self._default is NoValue:
if self._default is NO_VALUE:
raise NoDefaultValue(f'Field {self.name} has no default value.')
value = cast('_TS', self._default)

Expand Down Expand Up @@ -788,7 +788,7 @@ def default(self, value: '_TC | NoValueType') -> 'None':
@default.deleter
def default(self) -> 'None':
"""Delete field default value."""
self._field.default = NoValue
self._field.default = NO_VALUE

@property
def template(self) -> 'str':
Expand Down
Loading
Loading