Sentinel Objects

pcapkit.corekit.sentinels is the shared home of every module-level singleton sentinel this package defines for itself: a value recognised only by identity (value is SENTINEL), so it cannot be confused with a value a 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 (#1744).

Each behaves like a built-in singleton such as 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 (#1842).

Added in version 1.5.0: NoValueType moves here from pcapkit.corekit.fields.field (#911), and NO_VALUE replaces its NoValue there (#937). Sentinel is the public base of every sentinel type (#1842).

class pcapkit.corekit.sentinels.Sentinel[source]

Bases: object

Abstract base of every sentinel type, so each behaves like the type of None.

A subclass sets _name to the name of the module-level variable that holds its one instance, which is what makes it concrete, and gets the rest from here and from SentinelMeta:

  • __new__() hands back the one instance, so calling the type again cannot produce a second object that fails an is check. It returns the existing instance rather than raising, as type(None)() returns None, so a caller who builds one by mistake still gets the sentinel.

  • __copy__() and __deepcopy__() return self.

  • __reduce__() returns _name, as Ellipsis.__reduce__() returns 'Ellipsis', so pickle stores the instance as a reference to that module-level name and loads it back as the same object, at every protocol. Guarding __new__() alone would not cover protocols 0 and 1, which rebuild through copyreg._reconstructor() and so call object.__new__() directly.

  • __repr__() returns _name itself, as repr(None) is 'None'. str gives the same, since nothing here overrides object.__str__().

  • __setattr__() and __delattr__() raise AttributeError for every name, as None.x = 1 does, and __slots__ is empty, so an instance has no __dict__ to change either.

  • SentinelMeta keeps the type itself immutable, and a concrete one free of subclasses.

__bool__ is left to each sentinel, since only some of them stand for an absent value.

Public, as the library’s other abstract bases are, so that an annotation or an isinstance() check can name every sentinel at once. Every sentinel the library itself uses is defined in this module (#911).

A pickle written before #1842 still loads. A pickle of NULL names _get_null, kept for it, and a pickle of another sentinel at protocol 2 or above rebuilds through __new__(); both load as the sentinel. A pickle of another sentinel at protocol 0 or 1 rebuilds through copyreg._reconstructor(), which no hook on the class can reach, so it loads as a second instance of the type.

Added in version 1.5.0.

static __new__(cls)[source]

Return the one instance of this class there will ever be.

Raises:

TypeError – If cls is abstract, as object.__new__() does.

Return type:

Self

__repr__()[source]

Return the sentinel’s own name, as repr(None) is 'None'.

Return type:

str

__copy__()[source]

Return self – there is, and only ever will be, one of these.

Return type:

Self

__deepcopy__(memo)[source]

Return self, for the same reason as __copy__().

Parameters:

memo (dict[int, Any]) – The copy.deepcopy() memo table. Unused: returning self needs no entry, since nothing about this object is ever copied.

Return type:

Self

__reduce__()[source]

Reduce to _name, which pickle stores as a global reference.

Return type:

str

__setattr__(name, value)[source]

Refuse, as None.x = 1 does: a sentinel has no state to change.

Parameters:
  • name (str) – Attribute name.

  • value (Any) – Attribute value.

Raises:

AttributeError – For every name.

Return type:

NoReturn

__delattr__(name)[source]

Refuse, as del None.x does.

Parameters:

name (str) – Attribute name.

Raises:

AttributeError – For every name.

Return type:

NoReturn

class pcapkit.corekit.sentinels.NullType[source]

Bases: Sentinel

Type of NULL, the omitted-class_/module sentinel.

A distinct class rather than a plain str such as '(null)', so that is comparisons against it mean what they say: no str a caller passes, including one that happens to spell '(null)' itself, can be this sentinel. See GitHub issue #833.

Sentinel keeps it to one instance. ModuleDescriptor.klass checks is NULL; a second instance would slip past it to getattr() and downgrade a clean ProtocolError into a bare TypeError (attribute name must be string, not 'NullType').

A caveat rather than a defect: importlib.reload() on this module re-executes the class statement and NULL = NullType() below, producing a second singleton that the reloaded code compares against correctly but that every module which already imported the pre-reload NULL still holds – so a comparison spanning the reload sees two “singletons” that are not each other. ModuleDescriptor.klass faces exactly this class of problem for the class it resolves, which is why it re-reads 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. 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 pcapkit.corekit.sentinels after import. Pickling a pre-reload NULL raises pickle.PicklingError, since it is no longer the object its name refers to.

Reloading pcapkit.corekit.module instead has no effect on the singleton at all. That module only imports NULL from here, and re-running an already-satisfied from ... import reads the current binding in sys.modules rather than re-executing anything, so it neither mints a new instance nor loses the old one.

pcapkit.corekit.sentinels.NULL = NULL

Sentinel for an omitted class_ argument to the register_* helpers in pcapkit.foundation.registry.protocols and pcapkit.foundation.registry.foundation. Housed here with the package’s other sentinels, per the module docstring above.

Type:

NullType

class pcapkit.corekit.sentinels.NoValueType[source]

Bases: Sentinel

Type of NO_VALUE, the default value for pcapkit.corekit.fields.

Housed here per GitHub issue #911; FieldBase.default documents it as the field-default sentinel.

Changed in version 1.5.0: A singleton: calling this type, copy.deepcopy() and pickle give back NO_VALUE rather than a second instance (#1842). It reprs as NO_VALUE, its own name, as repr(None) is 'None', rather than as an object address, which is what a field constructor’s signature and a parsed schema’s repr show for an unset value (#1841). Its attributes, and its type’s, cannot be set or deleted, and the type cannot be subclassed.

pcapkit.corekit.sentinels.NO_VALUE = NO_VALUE

Default value for FieldBase.default.

Type:

NoValueType

class pcapkit.corekit.sentinels.NoDefaultType[source]

Bases: Sentinel

Type of NO_DEFAULT, the omitted-default sentinel for EnumLookup.get.

A dedicated class rather than a bare object, as review of the work for #857 asked, following the house convention. A bare object compares under is exactly as safely as a dedicated class with no __eq__ of its own does; what it lacks is a readable representation: it prints as <object object at 0x...> in a signature, in help(), and in a traceback, where NoDefaultType() – via the __repr__ Sentinel gives it – prints as NO_DEFAULT. The names follow the convention every sentinel here shares, settled by GitHub issue #937: <Name>Type for the class and SCREAMING_SNAKE for the instance.

Sentinel keeps it to one instance. That guards against a caller writing registry.get(key, default=NoDefaultType()) – perhaps not realising NO_DEFAULT already exists – and getting back a second, non-identical sentinel that silently fails is NO_DEFAULT inside EnumLookup.get, so their call is treated as supplying a real (if useless) default instead of the no default they meant.

A reload caveat that is dormant rather than live. 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 False where it once read True. EnumLookup.get is such a consumer but is not affected: its fallback is guarded by default not in cls._value2member_map_ (added by GitHub issue #864 to stop default minting a new member), and a NoDefaultType instance, stale or fresh, is never a registered enum value, so the guard reads True for it either way and get re-raises the original lookup error regardless of which NO_DEFAULT a caller’s default is stale against. Measured:

>>> Hardware.get('Definitely-Not-A-Member')              # before reload
EnumKeyError: "'Definitely-Not-A-Member' is not a valid Hardware"
>>> importlib.reload(pcapkit.corekit.sentinels)
>>> Hardware.get('Definitely-Not-A-Member')               # after reload
EnumKeyError: "'Definitely-Not-A-Member' is not a valid Hardware"

The hazard is not theoretical elsewhere: reload staleness is a tracked defect class here for other constructs – see pcapkit.protocols.protocol.ProtocolBase._lookup_next_layer()’s own docstring note on why it declines to memoise a resolved class, and 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. A future comparison site written the vulnerable way – a bare is NO_DEFAULT with no independent guard behind it – would reproduce it. Nothing in this package reloads pcapkit.corekit.sentinels after import, but that is a caveat to keep honest rather than a guarantee this class enforces.

Note

Reloading pcapkit.corekit.enum alone does not produce a fresh NO_DEFAULT. That module does not define NoDefaultType; it reads NO_DEFAULT off this module once, at its own import time, into its own module global, and a reload just re-runs that read, which – so long as this module has not also been reloaded – fetches back the identical object. Producing a fresh one takes reloading this module, where the class statement lives; and since from ... import binds a copy rather than a live alias, pcapkit.corekit.enum’s own global stays pointed at whatever it read until something re-runs that import.

Unlike both NullType and NoValueType, this deliberately does not define __bool__. Both of those model an absent value, so reading falsy in a boolean context is the point. NO_DEFAULT models something different: a marker meaning no default was supplied, checked exclusively by is at EnumLookup.get’s comparison sites – nothing here ever evaluates it for truthiness. Giving it __bool__ -> False for symmetry with those 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 NO_DEFAULT the same way it reads a caller’s genuine falsy default – 0, '', None or False – which is the collision with a genuine default that #857 exists to rule out. 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.

pcapkit.corekit.sentinels.NO_DEFAULT = NO_DEFAULT

The default argument value that means no default, i.e. let an unresolvable key propagate its lookup error rather than falling back. See NoDefaultType for why this is a dedicated class rather than a bare object, and why it does not define the __bool__ that its pcapkit.corekit siblings do.

Type:

NoDefaultType

Note

AbsentType and ABSENT are private – never imported outside pcapkit.protocols.protocol, and named in no module’s __all__. For SCREAMING_SNAKE consistency with the other sentinels (see Naming a Sentinel), neither carries the leading underscore that would hide it from Sphinx, so both are documented below and marked private.

class pcapkit.corekit.sentinels.AbsentType[source]

Bases: Sentinel

Type of ABSENT, the absent-key sentinel.

A distinct class rather than a bare 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 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 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. Private, although the names carry no leading underscore: GitHub issue #937 put every sentinel on the CamelCase/SCREAMING_SNAKE convention, and the owner’s ruling on GitHub issue #719 held that documenting ABSENT as a private type and class, not for public use, is enough to replace the underscore. Nothing outside pcapkit.protocols.protocol reads ABSENT, from here or from there, and neither this module’s nor that module’s __all__ names either one. This docstring, and the “Naming a Sentinel” section of docs/source/contributing/conventions/sentinel-convention.rst, are what record that.

pcapkit.corekit.sentinels.ABSENT = ABSENT

Absent-versus-None sentinel for pcapkit.protocols.protocol._declared_keywords() to read a class’s own __keywords__ out of its __dict__, where None is itself a meaningful value – the opt-out that says the class cannot enumerate its keywords, cf. ProtocolBase.__keywords__. Never leaves pcapkit.protocols.protocol, which imports it from here for exactly that one read. Private by documentation rather than by a leading underscore – see AbsentType’s own docstring.

Type:

AbsentType

Internal Definitions

class pcapkit.corekit.sentinels.SentinelMeta(name, bases, attrs, /, **kwargs)[source]

Bases: ABCMeta

Metaclass of Sentinel, so every sentinel type acts as type(None) does.

  • A concrete sentinel type – one that sets _name, and so has no abstract member left – cannot be subclassed, either by a class statement or by register(). Both raise TypeError, as class X(type(None)) does. An abstract one, Sentinel included, can be.

  • A concrete sentinel type makes its one instance while its class statement runs, and keeps it as _instance.

  • Every sentinel type is immutable once its class statement has run. Setting or deleting any attribute on it raises TypeError, as type(None).x = 1 does, so _name, _instance and the methods every sentinel inherits stay as they were defined. The one exception is _abc_negative_cache and _abc_negative_cache_version, which _py_abc, the fallback abc uses without the C _abc module, rebinds during an isinstance() check (#1960).

A concrete sentinel type also gets __final__ set to True here. final() would record the same, but a class decorator runs only after the class statement, when the type already refuses the write, and typing.final() skips an attribute it cannot set.

These guards, and Sentinel’s own, are Python-level hooks. So type.__setattr__() called directly on the type, or object.__setattr__() setting __class__ on an instance, still reaches past them, where nothing reaches past a built-in type’s.

Added in version 1.5.0.