Sentinel Objects¶
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.
Each of the three below used to live beside the one class that consumed it –
NullType in pcapkit.corekit.module, NoValueType in
pcapkit.corekit.fields.field and NoDefaultType in
pcapkit.corekit.enum – until GitHub issue #911 moved all four
definitions here, the private AbsentType/ABSENT included. Each
original module keeps a re-export, so every existing from <module> import
<name> keeps working unchanged.
- class pcapkit.corekit.sentinels.NullType[source]¶
Bases:
objectType of
NULL, the omitted-class_/modulesentinel.A distinct class rather than a plain
str– which is what the registry helpers inpcapkit.foundation.registry.protocolsandpcapkit.foundation.registry.foundationused to define, independently of each other – so thatiscomparisons against it mean what they say: nostra 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:
__new__()always hands back the one instance that already exists, rather than building a new one, so no caller – direct, orcopy/picklereconstructing an instance behind the scenes – can end up holding a second object that fails anis NULLcheck downstream.ModuleDescriptor.klassmakes 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 ownNULL = 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
copy.deepcopy(),copy.copy()andpickleunhandled: none of them constructs a new instance by callingNullType()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:copy.copy()andcopy.deepcopy()check for__copy__()/__deepcopy__()before ever falling back to reduction, so__deepcopy__()in particular has to be defined – its absence is the actual defect this class used to have: deepcopying aModuleDescriptorrecursed into this sentinel, reduced it, and rebuilt a second, non-identicalNullTypethat then read as an ordinary attribute name togetattr(), downgrading a cleanProtocolErrorinto a bareTypeError(attribute name must be string, not 'NullType').pickleprotocols 2 and up reconstruct throughcls.__new__(cls), which the guarded__new__()already keeps to one instance – but protocols 0 and 1 reconstruct throughcopyreg._reconstructor(), which callsobject.__new__()directly, bypassing__new__()entirely.__reduce__()is defined so that every protocol, not only the ones that happen to go through this class’s own__new__(), is routed through the same module-level getter instead of through reconstruction at all.
A caveat rather than a defect:
importlib.reload()on this module re-executesNULL = NullType()below, producing a second singleton that the reloaded code compares against correctly but that every module which already imported the pre-reloadNULLstill holds – so a comparison spanning the reload sees two “singletons” that are not each other.ModuleDescriptor.klassfaces exactly this class of problem for the class it resolves, which is why it re-readssys.moduleson 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-#833strsentinel 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 reloadspcapkit.corekit.sentinelsafter import.A second caveat, specific to this class now living apart from its one caller-visible re-export: reloading
pcapkit.corekit.moduleitself – rather than this module – no longer has any effect on the singleton at all. That module now only re-importsNULLandNullTypefrom here, and re-running an already-satisfiedfrom ... importreads the current binding insys.modulesrather 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.
- pcapkit.corekit.sentinels.NULL = <NULL>¶
Sentinel for an omitted
class_argument to theregister_*helpers inpcapkit.foundation.registry.protocolsandpcapkit.foundation.registry.foundation. Housed here, alongside the package’s other sentinels, rather than inpcapkit.corekit.modulewhere it used to live – per the owner’s ruling on GitHub issue #911, see the module docstring above.pcapkit.corekit.modulekeeps a re-export so every existingfrom pcapkit.corekit.module import NULLkeeps working.- Type:
- class pcapkit.corekit.sentinels.NoValueType[source]¶
Bases:
objectType of
NO_VALUE, the default value forpcapkit.corekit.fields.Housed here per GitHub issue #911 rather than in
pcapkit.corekit.fields.field, where it used to be defined and whereFieldBase.defaultstill documents it as the field-default sentinel.
- pcapkit.corekit.sentinels.NO_VALUE¶
Default value for
FieldBase.default.pcapkit.corekit.fields.fieldkeeps a re-export, since that attribute’s own documentation is a published contract naming this object. Renamed fromNoValuetoNO_VALUEby GitHub issue #937, which normalised all four sentinel objects to SCREAMING_SNAKE.- Type:
- class pcapkit.corekit.sentinels.NoDefaultType[source]¶
Bases:
objectType of
NO_DEFAULT, the omitted-defaultsentinel forEnumLookup.get.A dedicated class rather than a bare
object, per the owner’s ruling on #859: “use dedicated class rather than bare object. Follow the house convention.” A bareobjectcompares underisexactly 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 bareobjectactually lacks is a readable representation: it prints as<object object at 0x...>in a signature, inhelp(), and in a traceback, whereNoDefaultType()– via__repr__()below – prints as<NO_DEFAULT>.Named
NoDefaultTypefor the class because that half of the house convention is settled: bothNullTypeandNoValueTypeuse<Name>Type. At the time, the instance’s own name was not similarly settled – the owner’s follow-up on #859 was explicit thatNULL(SCREAMING_CASE) andNoValue(CapWords) disagreed, and “mainly depends on how we need it.” The need here was continuity:NO_DEFAULTwas already the name onmain– referenced inEnumLookup.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’sSCREAMING_CASEwas the closer match regardless, sinceNO_DEFAULTwas already spelled that way – and GitHub issue #937 later settled the question this paragraph left open:NoValuebecameNO_VALUEand_AbsentbecameABSENT, so every instance name now agrees on SCREAMING_SNAKE.Genuinely a singleton, not merely a class this module happens to instantiate once:
__new__()always hands back the one instance that already exists, rather than building a new one. That guards against a caller writingregistry.get(key, default=NoDefaultType())– perhaps not realisingNO_DEFAULTalready exists – and getting back a second, non-identical sentinel that silently failsis NO_DEFAULTinsideEnumLookup.get, so their call is treated as supplying a real (if useless) default instead of the no default they meant. With the guard,NoDefaultType()always returns the one canonicalNO_DEFAULT, so that mistake self-corrects.Unlike
NullType, this does not also define__copy__,__deepcopy__or__reduce__– but not becausecopy.deepcopy()orpickle“bypass”__new__(); they do not, for protocol 2 and above.object.__reduce_ex__()at protocol 2 reduces throughcopyreg.__newobj__(), which reconstructs by callingcls.__new__(cls)– exactly the guarded path above – socopy.copy,copy.deepcopyand 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
__new__()cannot see is pickle protocol 0 (and 1), which reduces throughcopyreg._reconstructor()instead, and that callsobject.__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
NullType’s own__reduce__()exists to close, becauseNULLis stored as aModuleDescriptorfield that a caller’s owncopy.deepcopy()orpicklecall can walk into and reconstruct.NO_DEFAULTis 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 – fromEnumLookup.get’s own bound parameter default (EnumLookup.get.__defaults__[0], or any subclass’s, e.g.Hardware.get.__func__.__defaults__[0]) and frominspect.signature(Hardware.get).parameters['default'].default– but neither is a field any object here gets pickled as, and both still survivecopy.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.
importlib.reload()on this module re-executes bothclass NoDefaultType:andNO_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 bareheld_default is NO_DEFAULTcomparison against the post-reload global then readsFalsewhere it once readTrue.EnumLookup.getlooked exactly like such a consumer before GitHub issue #864: an unrecognised, non-NO_DEFAULTvalue used to fall through tocls(default), so a stale sentinel handed to that call could raise aValueErrora caller had no reason to expect from an omitted argument. #864 closed a different hole –defaultcould mint a new member – by replacing that call with adefault not in cls._value2member_map_guard, and the guard happens to close this one too: aNoDefaultTypeinstance, stale or fresh, is never a registered enum value, so the guard’snot inhalf readsTruefor it either way andgetre-raises the original lookup error correctly regardless of whichNO_DEFAULTa 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
ValueError– was already wrong onmainatd31c0aaf6, independently of the move: it described the pre-#864cls(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:
-1never had this failure mode at all, since-1 == -1compares by value rather than identity, and reload staleness is a tracked defect class here for other constructs – seepcapkit.protocols.protocol.ProtocolBase._lookup_next_layer()’s own docstring note citing GitHub issues #425, #428 and #560, andtests.protocols.test_dispatch_default_resolution_unit’s owntest_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 bareis NO_DEFAULTwith no independent guard behind it, the way #864’s fix itself was not – would still reproduce it. “Nothing in this package reloadspcapkit.corekit.sentinelsafter 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.
pcapkit.corekit.enumno longer definesNoDefaultTypeitself; it only readsNO_DEFAULToff this module once, at its own import time, into its own module global. Reloadingpcapkit.corekit.enumalone 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 freshNO_DEFAULTat all now takes reloading this module, where the class statement lives; reloading onlypcapkit.corekit.enumis not enough on its own, becausefrom ... importbinds 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
NullTypeandNoValueType, this deliberately does not define__bool__. Both of those model an absent value, so reading falsy in a boolean context is the point.NO_DEFAULTmodels something different: a marker meaning no default was supplied, checked exclusively byisatEnumLookup.get’s two comparison sites – nothing here ever evaluates it for truthiness. Giving it__bool__ -> Falsefor symmetry with the other two would invite exactly the conflation this sentinel exists to rule out: code that writesif not default:instead ofif default is NO_DEFAULT:would then readNO_DEFAULTthe same way it reads a caller’s genuine falsy default –0,'',NoneorFalse– which is the exact collision-1used to cause under==and the reason #857 exists. Leaving__bool__undefined makesNoDefaultType()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
defaultargument value that means no default, i.e. let an unresolvable key propagate its lookup error rather than falling back. SeeNoDefaultTypefor why this is a dedicated class rather than a bareobject, why it is a guarded singleton, and why it defines neither the copy/pickle hooks nor the__bool__that itspcapkit.corekitsiblings do.pcapkit.corekit.enumkeeps a re-export, sinceEnumLookup.getnames this object in its signature and docstring.- Type:
Note
AbsentType and ABSENT are private – never imported outside
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 Naming a sentinel) 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.
- class pcapkit.corekit.sentinels.AbsentType[source]¶
Bases:
objectType of
ABSENT, the absent-key sentinel.A distinct class rather than a bare
objectso 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 whereobject()would give it nothing. It followsNoValueType, 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 ofFieldBase.defaultand 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_ABSENTtoABSENTjust document it as private type/class in the documentation and not for public use is enough.” So this class andABSENTstay exactly as private as they were: nothing outsidepcapkit.protocols.protocolreadsABSENT, 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 ofdocs/source/contributing/conventions/sentinel-convention.rst, are what now records that fact in place of the leading underscore.
- pcapkit.corekit.sentinels.ABSENT = <absent>¶
Absent-versus-
Nonesentinel forpcapkit.protocols.protocol._declared_keywords()to read a class’s own__keywords__out of its__dict__, whereNoneis itself a meaningful value – the opt-out that says the class cannot enumerate its keywords, c.f.ProtocolBase.__keywords__. Never leavespcapkit.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 – seeAbsentType’s own docstring for why, per GitHub issue #937.- Type: