Enumeration Base¶
pcapkit.corekit.enum contains the two bases every enumeration in this
library is meant to inherit from: EnumLookup, the bare lookup half
shared by open registries and closed sets alike, and EnumRegistry,
which adds the mutating half every generated enumeration under
pcapkit.const inherits.
- class pcapkit.corekit.enum.EnumLookup[source]¶
Bases:
objectBare lookup protocol, shared by open registries and closed sets alike.
Carries
get(),get_all()and the_validate_value()guard – everything an enumeration needs in order to be read by name or by value, and nothing that could grow it.EnumRegistryadds the mutating half on top; a closed enumeration inherits this one directly and so is never handed aregisterit would have to refuse.This is a plain mix-in rather than an
Enumsubclass, because an enumeration that already has members cannot be subclassed. Mixed in before the member type –class Foo(EnumRegistry, IntFlag), orclass Bar(EnumLookup, IntEnum)– it contributes methods only, soaenumstill resolves the member data type from the enumeration base:intforIntEnumandIntFlag,strforStrEnum. That is what lets one base serve all three, where a generated template fragment would have needed a separate rendering per member type.Because both tiers are plain classes, inserting this one above
EnumRegistryleaves the member data type exactly where it was:class Foo(EnumRegistry, IntFlag)resolves asFoo -> EnumRegistry -> EnumLookup -> IntFlag -> int -> ..., so_member_type_still comes from the enumeration base and not from anything in this module. Had this tier subclassedEnumin order to “be an enum”, it would have become the member type itself and broken all three shapes at once.The methods deliberately touch only
_member_map_,_member_names_and_value2member_map_, which bothenumandaenummaintain, so nothing on this tier depends onaenuminternals at all – the oneextend_enum()call in this module belongs toEnumRegistry, which is the tier that mutates.- classmethod _validate_value(value)[source]¶
Hook: reject
valueif this enumeration’s contract does not allow it.The owner’s requirement on GitHub issue #877, verbatim: “there must be some sort of range validation logic for the inherited classes to hook in.” This is that hook, and it is what the bare tier carries instead of
register: what values are legal is something every enumeration has an opinion on, whereas who may add one is only an open registry’s concern.The base implementation accepts everything, because a base cannot know any subclass’s range. Overriding it is how a subclass states one – the shape
_missing_spells by hand across the generated registries today:@classmethod def _validate_value(cls, value: 'Any') -> 'None': if not (isinstance(value, int) and 0 <= value <= 0xFF): raise EnumValueError(f'{value!r} is not a valid {cls.__name__}')
An override raises or returns; it must never normalise. The return type is
Nonedeliberately rather than the validated value, so that this hook cannot become a converter: a subclass that returned a changed value here would silently alter what a lookup resolves to, which is exactly the case-folding the owner’s ruling on GitHub issue #877 rules out – “enum should honour and keep their original writings as in the registrars.” Case handling belongs in a deliberategetoverride with an RFC behind it, not in a validation hook.Raise from
pcapkit.utilities.exceptions, per the same issue’s ruling that in-library code raises in-library exceptions –EnumValueErroris the fitting one and is already what the closed enumerations inpcapkit.protocols.internet.mhraise. Note what that buys on theget()path:EnumValueErrorsubclassesValueError, so a rejection here is caught byget()’s ownexcept ValueErrorand falls back todefaultjust as any other unresolvable value does. An override raising something outside that hierarchy would instead propagate pastdefault, which is a real difference in behaviour rather than a stylistic preference. With no usabledefault, a rejection from this hook reaches the caller exactly as the override raised it –get()re-raises an in-libraryValueErrorunchanged rather than re-wrapping it, so the override’s own message and the single log record it already emitted are what the caller sees.Called from exactly two places, and the omissions are deliberate:
get(), immediately beforecls(key)– the one point at which a lookup can reach a subclass’s_missing_and mint. Thestr-key path does not call it, because that path never callscls(key): it resolves against the already-populated lookup tables only, where every value present is legal by construction, so there is nothing left to validate.EnumRegistry.register(), before minting – the one point at which a caller can introduce a value no member carries yet.
EnumRegistry.register_alias()does not call it, and does not need to: it refuses anyvaluethat is not already registered, so the value it aliases has necessarily passed validation already.EnumRegistry._unregistered_member()does not call it either, because its callers are the subclasses’ own_missing_bodies, which already range-check before delegating here – validating again would double the check without being able to disagree with it.Deliberately carries no
Raises:clause, because this implementation raises nothing at all – an override is what raises, and documenting an exception here that this body cannot produce is exactly the phantomtests.test_docstring_contract.DocstringRaisesTestsrejects. An override adds its own clause naming what it rejects.- Parameters:
value (
Any) – Candidate value to check.- Returns:
Nothing. A value this enumeration allows is reported by returning normally; a value it does not is reported by raising.
- classmethod _validate_value(value)[source]¶
Hook: reject
valueif this enumeration’s contract does not allow it.The owner’s requirement on GitHub issue #877, verbatim: “there must be some sort of range validation logic for the inherited classes to hook in.” This is that hook, and it is what the bare tier carries instead of
register: what values are legal is something every enumeration has an opinion on, whereas who may add one is only an open registry’s concern.The base implementation accepts everything, because a base cannot know any subclass’s range. Overriding it is how a subclass states one – the shape
_missing_spells by hand across the generated registries today:@classmethod def _validate_value(cls, value: 'Any') -> 'None': if not (isinstance(value, int) and 0 <= value <= 0xFF): raise EnumValueError(f'{value!r} is not a valid {cls.__name__}')
An override raises or returns; it must never normalise. The return type is
Nonedeliberately rather than the validated value, so that this hook cannot become a converter: a subclass that returned a changed value here would silently alter what a lookup resolves to, which is exactly the case-folding the owner’s ruling on GitHub issue #877 rules out – “enum should honour and keep their original writings as in the registrars.” Case handling belongs in a deliberategetoverride with an RFC behind it, not in a validation hook.Raise from
pcapkit.utilities.exceptions, per the same issue’s ruling that in-library code raises in-library exceptions –EnumValueErroris the fitting one and is already what the closed enumerations inpcapkit.protocols.internet.mhraise. Note what that buys on theget()path:EnumValueErrorsubclassesValueError, so a rejection here is caught byget()’s ownexcept ValueErrorand falls back todefaultjust as any other unresolvable value does. An override raising something outside that hierarchy would instead propagate pastdefault, which is a real difference in behaviour rather than a stylistic preference. With no usabledefault, a rejection from this hook reaches the caller exactly as the override raised it –get()re-raises an in-libraryValueErrorunchanged rather than re-wrapping it, so the override’s own message and the single log record it already emitted are what the caller sees.Called from exactly two places, and the omissions are deliberate:
get(), immediately beforecls(key)– the one point at which a lookup can reach a subclass’s_missing_and mint. Thestr-key path does not call it, because that path never callscls(key): it resolves against the already-populated lookup tables only, where every value present is legal by construction, so there is nothing left to validate.EnumRegistry.register(), before minting – the one point at which a caller can introduce a value no member carries yet.
EnumRegistry.register_alias()does not call it, and does not need to: it refuses anyvaluethat is not already registered, so the value it aliases has necessarily passed validation already.EnumRegistry._unregistered_member()does not call it either, because its callers are the subclasses’ own_missing_bodies, which already range-check before delegating here – validating again would double the check without being able to disagree with it.Deliberately carries no
Raises:clause, because this implementation raises nothing at all – an override is what raises, and documenting an exception here that this body cannot produce is exactly the phantomtests.test_docstring_contract.DocstringRaisesTestsrejects. An override adds its own clause naming what it rejects.- Parameters:
value (
Any) – Candidate value to check.- Returns:
Nothing. A value this enumeration allows is reported by returning normally; a value it does not is reported by raising.
- classmethod get(key, default=<NO_DEFAULT>)[source]¶
Resolve
keyto the canonical member.A shortcut for the
[]operation, per the ruling on #842: given a name it iscls[key], and given a value it iscls(key). Either way the answer is the canonical member – subscripting an alias returns the member the alias points at, not a separate object – so two names for one assignment resolve to one enum.It never mints while resolving
default;keymay still mint through a_missing_that GitHub issue #775’s ruling deliberately kept minting, on one registry (CGAType) – the ruling’s final round converted the other two it originally held out,EtherTypeandSocket, so they no longer mint on any path either. Registering a member any other way isregister()’s job and nobody else’s, which is the ruling #775 exists to carry out: “so that we dont create registered enums out of unrecognised/unregistered values, unless user/caller explicitly created them”. A value inside a registry’s declared-but-unassigned range still resolves, through that registry’s own_missing_and_unregistered_member(), to a member that is deliberately absent from the lookup tables – true outside the one registry named above, where such a value instead lands in both tables, exactly asregister()would leave it – for a non-strkey; thestrcase is qualified below. Both describekeyresolution only.defaultnever reaches_missing_on either branch: a declared-but-unassigneddefaultdoes not resolve to an unregistered member the way such akeydoes – it simply does not resolve, and the lookup errorkeyitself would have raised propagates instead.For a
strkey, a name match wins over a value match – the two are checked in that order, so a string that happens to be both a member’s name and a different member’s value resolves to the name’s member, matching what already happened for a name that resolves today. The value side of that check is a plain_value2member_map_lookup, notcls(key): on a registry whose own_missing_mints for an unrecognised value, routing a failed name lookup through the constructor would let a mereget()call mint a permanent member where it previously just raised. Defensive rather than observed: of the 127 classes that reach this method – 125 until GitHub issue #880’s own PR addedpcapkit/const/ngap/procedure_code.pyandpcapkit/const/ngap/protocol_ie.py, remeasured while auditing GitHub issue #903 – thestr-valued ones (Command,FEATCode,Method,OptionType,AppTypeand its four transport subclassesTCP,UDP,SCTPandDCCP– completing that set as of GitHub issue #860’s own PR 2 – and, newest of them,TLSKeyLabel, which GitHub issue #877’s own thread reclassified from a hand-written helper to a generated registry once RFC 9850 §4.2 turned its member list into a live IANA registry) no longer mint on any path, so no live witness exists in this tree today. The one registry that still mints directly viaextend_enum(),CGAType, isint-valued, so astrname could not reach its mint branch even if this restriction did not exist; it is not an exception to it, just not reachable by it. GitHub issue #775’s final round converted the other two that used to share this footnote,SocketandEtherType, soCGATypeis now the only one left. This is about a futurestr-valued registry (or a present one whose_missing_someday changes) reaching this base with a minting_missing_of its own, which the restriction below is written to stay correct for regardless. Restricting the value side ofkeyto an already-registered value keeps that side non-minting on everystr-valued registry, not only the ones without a minting_missing_. Since #864, that is no longer merely a claim about the value side alone:defaultresolves through the same kind of_value2member_map_lookup rather thancls(default), so for astrkey every path through this method – name, value anddefaultalike – is non-minting.That restriction has a cost the paragraph above glosses over: a declared-but-unassigned value – the case resolved there through
_missing_and_unregistered_member()without either lookup table growing – is for that exact reason invisible to the_value2member_map_check above. Such a value resolves throughcls(value)but not throughget(value)whenvalueis astr; the non-strpath below has no such gap, since it always callscls(key)and so always reaches_missing_. Closing that gap here would mean callingcls(key)for astrvalue too, which reopens the exact minting hazard the paragraph above exists to avoid – so the asymmetry is deliberate, not an oversight.The non-
strpath calls_validate_value()immediately beforecls(key), which is the only point at which this method can reach a subclass’s_missing_, so a subclass that declares a range gets it checked before the constructor rather than after. The base hook accepts everything, so this changes nothing for a subclass that does not override it. A rejection raised asEnumValueError– or any otherValueErrorsubclass – is caught by the sameexceptthat catches an ordinary failed construction, and so falls back todefaulton the same terms; thestrpath does not call the hook, for the reason given on_validate_value()itself.Both failure paths raise from
pcapkit.utilities.exceptionsrather than a builtin, per the owner’s ruling on GitHub issue #923: “Either ``ValueError`` or ``KeyError``, that’s depending on how stdlib’s ``Enum`` would raise on these circumstances. And we should raise one from ``pcapkit.utilities.exceptions`` rather builtin exceptions.” The shape is unchanged by that ruling and deliberately so – a name miss staysKeyError-derived and a value missValueError-derived, matchingE['nosuch']andE(999)on a stdlibEnum, and matching the 119 of this tree’s 127 concrete subclasses that already answered a name miss that way. Only the provenance changed, so everyexcept KeyErrorandexcept ValueErroraround a call to this method keeps catching.Two details of that conversion are worth stating, since neither is visible from the exception type alone:
The name miss is raised quietly –
EnumKeyErrorwithquiet=True, so nothing is logged andsys.tracebacklimitis left alone. That is not a cosmetic choice: this method’s name miss is in-library control flow at six call sites, and atget()it is part of a successful call – that override catches it in order to mint. A loud error there would put alogging.CRITICALrecord on every such call and setsys.tracebacklimitto0process-wide, which is exactly the GitHub issue #362 defectBaseErrordocumentsquietfor. The value miss takes no such fallback anywhere in this tree, so it stays loud.An in-library rejection propagates unchanged. A
ValueErrorthat is already aBaseError– typicallyEnumValueErrorfrom a subclass’s_validate_value()– is re-raised as it stands rather than wrapped, so the subclass’s own message survives and the error is logged once instead of twice. Onlyaenum’s andenum’s own “no member carries this value” is converted. This is the same discriminationEnumField.post_processalready makes for the same reason.
- Parameters:
key (
Any) – Name or value to look up.default (
Any) – An already-registered value to fall back to whenkeydoes not resolve. Resolved through a plain_value2member_map_lookup, never throughcls(default), so it cannot mint – see #864.NO_DEFAULTstands for no default; that and adefaultnaming no registered member both fall through to the same lookup errorkeyitself would have raised.
- Return type:
Self- Returns:
The canonical member for
key, or fordefault.- Raises:
EnumValueError – If a value does not resolve and there is no usable default. Also what a subclass’s
_validate_value()rejection reaches the caller as, since that hook is documented to raise this very class and it is passed through rather than re-wrapped. AValueError, so anexcept ValueErrorcaller is unaffected.EnumKeyError – If a name does not resolve and there is no usable default. A
KeyError, so anexcept KeyErrorcaller is unaffected.
- classmethod get_all(key)[source]¶
Every member matching
key, canonical first.For a registry that maps one key to one member – which is every registry inheriting this base unmodified – that tuple holds exactly one entry, since an alias registered by
register_alias()is a second name for the canonical member rather than a second member. The method still exists here, per the ruling that all four “should always exist on the const enums”, and it is where a registry with genuinely several matches puts them:AppTypeoverrides it to return every service IANA assigns to a port.- Parameters:
key (
Any) – Name or value to look up.- Return type:
- Returns:
The canonical member, followed by any further distinct member carrying the same value.
- Raises:
EnumValueError – As
get()with no default, for a value.EnumKeyError – As
get()with no default, for a name.
- class pcapkit.corekit.enum.EnumRegistry[source]¶
Bases:
EnumLookupRegistry protocol shared by every constant enumeration under
pcapkit.const.EnumLookupabove carries the read half –get(),get_all()and_validate_value(), all inherited here unchanged. What this tier adds is the half that makes a registry open:register(),register_alias(),register_aliases(),_extend()and_unregistered_member().An enumeration inherits from here when it may grow at runtime, and from
EnumLookupdirectly when it may not. The owner’s ruling on GitHub issue #877 is what draws that line, verbatim: “My initial thought was to make them immutable - unless RFC/IANA says otherwise.”Mixed in ahead of the enum base exactly as before –
class Foo(EnumRegistry, IntFlag)– and gainingEnumLookupas a parent does not disturb that: both tiers are plain classes, so_member_type_still resolves past them to the enumeration base.- classmethod _extend(value, name)[source]¶
The raw
extend_enum()call, shared byregister()andregister_alias().Neither public method calls the other:
register()now refuses an already-registeredvaluebefore it would ever reach here, andregister_alias()depends on the opposite of that – it verifiesvalueis already registered and then relies on exactly the mint-or-alias behaviour this wraps to addnameas a further name for the existing member rather than a new one. Routing both through this shared, ungated call is what keeps that behaviour available toregister_alias()whileregister()still rejects it.- Parameters:
- Return type:
Self- Returns:
The member now reachable under
name, new or existing.- Raises:
ValueError – If
nameis already taken.aenumreports that asTypeError; it is translated so that the ways one call can fail are one exception type.
- classmethod _unregistered_member(value, name)[source]¶
Build a member absent from this registry’s own lookup tables.
Used by a registry’s
_missing_for a declared-but-unassigned value it resolves without anyone asking for a name, so that such a lookup no longer grows the registry – contrastregister(), the explicit path that still does.The member is constructed through
cls._member_type_, whichaenumsets from the enumeration base, so this servesint- andstr-valued registries alike without either having to say which it is.
- classmethod register(value, name)[source]¶
Mint a new member on this registry at runtime, under
name.The caller-named path, and the only one that grows the registry: “register mints new enum to the class at runtime with specified names - so we don’t have to guess blindly”. Contrast
get()and_missing_, which resolve without naming anything.Refuses a
valuethat already has a member. Without this guard,extend_enum()does not mint anything for an already-taken value –aenumtreats that as a request to alias the existing member under the caller’snameinstead, silently: the call returns the existing member,namebecomes reachable in__members__pointing at it, and_member_names_does not grow. That isregister_alias()’s own effect, reached through the wrong method and with nothing raised to say so – exactly the “guess blindly” this method exists to rule out. Membership is tested against_value2member_map_rather than by callingcls(value), for the same reasonregister_alias()tests it that way: a declared-but-unassigned value resolves through_missing_to an_unregistered_member()absent from that table, so a successful call proves nothing about whether a member already exists.Routes through
_validate_value()before minting, so a subclass that declares a range gets it enforced on the caller-named path too and not only on the lookup one. The duplicate check runs first: avaluethat already has a member is legal by construction, so the actionable “useregister_alias()instead” message is the better answer for it than a range complaint would be, and validation is left to guard only the genuinely new value that is about to be minted.- Parameters:
- Return type:
Self- Returns:
The newly registered member.
- Raises:
ValueError – If
valuealready has a member – useregister_alias()to add a further name for it instead.EnumValueError – If a subclass’s
_validate_value()rejectsvalue. The base implementation of that hook accepts everything, so this cannot arise on a registry that does not override it.ValueError – If
nameis already taken.aenumreports that asTypeError; it is translated so that the ways one call can fail are one exception type.
- classmethod _extend(value, name)[source]¶
The raw
extend_enum()call, shared byregister()andregister_alias().Neither public method calls the other:
register()now refuses an already-registeredvaluebefore it would ever reach here, andregister_alias()depends on the opposite of that – it verifiesvalueis already registered and then relies on exactly the mint-or-alias behaviour this wraps to addnameas a further name for the existing member rather than a new one. Routing both through this shared, ungated call is what keeps that behaviour available toregister_alias()whileregister()still rejects it.- Parameters:
- Return type:
Self- Returns:
The member now reachable under
name, new or existing.- Raises:
ValueError – If
nameis already taken.aenumreports that asTypeError; it is translated so that the ways one call can fail are one exception type.
- classmethod register_alias(value, name)[source]¶
Add
nameas a further name for the member already atvalue.Per the ruling, an alias “adds additional alias(es) to a given enum’s mapping” – so it needs an enum to be given, and this refuses a value no member carries rather than falling through to
register(). Asked whether that should hold generally, the maintainer’s answer was “actually i think it should always be for an existing member”, and on what an alias means away fromAppType: “For non-AppType registries, ‘Alias’ is custom/caller-opt-in names, which are not recorded in IANA registrars”. Minting under the name of an aliasing call would manufacture exactly the unrecorded member #775 removes.Membership is tested against
_value2member_map_rather than by callingcls(value): a declared-but-unassigned value resolves through_missing_to an_unregistered_member()that is deliberately absent from that table, so a successful call proves nothing about whether a member exists.An alias adds a name, not a member:
__members__grows by one while_member_names_, iteration and_value2member_map_are untouched. Calls_extend()directly rather thanregister(), which would now refuse this call outright –register()andregister_alias()testvalue’s membership for opposite outcomes, so neither can be the other’s implementation any more.- Parameters:
- Return type:
Self- Returns:
The existing member, now reachable under
nameas well.- Raises:
ValueError – If no member carries
value, or ifnameis already taken.
- classmethod register_aliases(value, *names)[source]¶
Add several aliases for the member at
value, left to right.- Parameters:
- Return type:
- Returns:
One entry per name in
names, each the aliased member.- Raises:
ValueError – As
register_alias(). Names before the failing one stay registered –extend_enum()has no transaction to roll back, and undoing it by hand would mean reaching further into enumeration internals than anything else here does.
- classmethod _unregistered_member(value, name)[source]¶
Build a member absent from this registry’s own lookup tables.
Used by a registry’s
_missing_for a declared-but-unassigned value it resolves without anyone asking for a name, so that such a lookup no longer grows the registry – contrastregister(), the explicit path that still does.The member is constructed through
cls._member_type_, whichaenumsets from the enumeration base, so this servesint- andstr-valued registries alike without either having to say which it is.
Auxiliaries¶
- class pcapkit.corekit.enum.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.enum.NO_DEFAULT = <NO_DEFAULT>¶
Type 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.