Enumeration Base¶
pcapkit.corekit.enum contains the bases every enumeration in this
library inherits from: EnumLookup, the lookup half shared by open
registries and closed sets alike, and its subclass EnumRegistry, which
adds the mutating half every registry under pcapkit.const inherits.
Added in version 1.5.0: The get each pcapkit.const enumeration carried as generated text
moves onto EnumLookup, and EnumRegistry adds register,
which is how a member is minted at runtime: outside
CGAType, looking up an unknown value does not mint
one (#775).
- 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
aenum.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:intforaenum.IntEnumandaenum.IntFlag,strforaenum.StrEnum. That is what lets one base serve each of them, 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 subclassedaenum.Enumin order to “be an enum”, it would have become the member type itself and broken every one of these 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 – theaenum.extend_enumcall 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.GitHub issue #877 requires some range-validation logic for the inheriting classes to hook into. 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, and every registry under
pcapkit.constdoes, its vendor generator emitting the check its_missing_makes (#1816) – in the shape:@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 ruling on GitHub issue #877 rules out: an enumeration keeps the original spellings its registrars use. 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. It is also quiet:get()always calls this hook inside aCaughtByCallerblock forValueError, so an override need not passquiet=Truefor its rejection to log nothing there (#1845). An override raising something outside that hierarchy would instead propagate pastdefault, unquieted, 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 is what the caller sees.Called from the places below, 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. Its rejection stays loud even whenregisterruns insideget()’s quiet block, since a refused mint is a fault to report rather than a lookup that missed.
Of the registries that override
EnumRegistry.register()to mint through a path of their own,OptionType.registerandAppType.registereach call it at the same point.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, onCGAType. Registering a member any other way isregister()’s job and nobody else’s, which is the ruling #775 exists to carry out: an unrecognised or unregistered value does not become a registered member unless a user or caller explicitly creates one. 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 outsideCGAType, 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. 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 should just raise. Defensive rather than observed: none of thestr-valued registries that reach this method (Command,FEATCode,Method,OptionType,AppTypeand its transport subclassesTCP,UDP,SCTPandDCCP, andTLSKeyLabel) mints on any path, so no live witness exists in this tree.CGAType, which still mints directly viaaenum.extend_enum, 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. 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_; anddefaultresolves through the same kind of_value2member_map_lookup rather thancls(default)(#864), 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.The hook and
cls(key)run inside aCaughtByCallerblock forValueError, because everyValueErrorthey raise is this method’s own lookup miss: caught here, then answered withdefaultor raised to the caller as the miss. A loudEnumValueErroran override constructs there is therefore quiet, though the override never passesquiet=True– the generated overrides cannot know which caller they serve. An error of any other class is not a lookup miss and escapes this method as it was raised, so it stays loud.Both failure paths raise from
pcapkit.utilities.exceptionsrather than a builtin, per the ruling recorded on GitHub issue #923: in-library code raises frompcapkit.utilities.exceptionsrather than a builtin, and whetherValueErrororKeyErrorapplies follows what stdlib’sEnumraises in the same circumstance: a name miss isKeyError-derived and a value missValueError-derived, matchingE['nosuch']andE(999)on a stdlibEnum, so everyexcept KeyErrorandexcept ValueErroraround a call to this method catches.These details are worth stating, since the exception type alone does not show them:
Every miss is quiet, by name or by value, whether answered with
defaultor raised: nothing is logged and no exception hook is installed. A raised miss still reaches the caller as an ordinary exception; left uncaught, it is printed by whichever hook is installed – the interpreter’s own, unless an earlier loud error installedpcapkit’s. The name miss isEnumKeyErrorwithquiet=True, because it is in-library control flow – in thegetoverride ofMethodit is part of a successful call, since that override catches it in order to build an unregistered member. The value miss is quiet through the block described above, as ruled on #1845 on the ground that this method is mostly called from protocol parsing: a value it cannot resolve is input for the caller to handle, not a fault to report. A loud miss would put alogging.CRITICALrecord on every such call – the GitHub issue #362 defectBaseErrordocumentsquietfor – and, outside development mode, installpcapkit’s exception hooks.EnumRegistry.register()refusing the same value 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 no second error is constructed. Onlyaenum’s andenum’s own “no member carries this value” is converted, into a quietEnumValueError. 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:
- 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. Quiet.EnumKeyError – If a name does not resolve and there is no usable default. A
KeyError, so anexcept KeyErrorcaller is unaffected. Quiet.
Changed in version 1.5.0: A value miss is quiet, whether
defaultanswers it or it is raised: it logs nothing and installs no exception hook, whileEnumRegistry.register()still reports the same range rejection loudly (#1845).
- 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, becauseget,get_all,registerandregister_aliasare all to exist on every const registry (GitHub issue #842), 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: an enumeration is immutable unless RFC or IANA says otherwise.Mixed in ahead of the enum base –
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 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: it mints a new member on the class at runtime under the
namethe caller specifies, so nothing has to be guessed (GitHub issue #842). Contrastget()and_missing_, which resolve without naming anything.Refuses a
valuethat already has a member. Without this guard,aenum.extend_enumdoes 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.That rejection is always loud.
get()quiets the same hook, because there the rejection is a lookup miss for the caller to handle; here it refuses a mint the caller explicitly asked for, which is a fault to report. So this calls the hook in aCaughtByCallerblock declaring that nothing is caught – loud even ifregisteris reached from inside aget(#1845).- Parameters:
- Return type:
- 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
aenum.extend_enumcall, shared byregister()andregister_alias().Neither public method calls the other:
register()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:
- 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 GitHub issue #842, an alias adds a further name to a given member’s mapping – so it needs an existing member to attach to, and this refuses a value no member carries rather than falling through to
register(). #842 settled on an alias always attaching to an existing member, leaving open whetherAppTypeor a concrete enumeration might need to alias a value no member carries.AppType’s override does not: it requires the port to already carry a member of that very registry. What an alias means also differs away fromAppType: on every other registry it is a custom name the caller opts into, not one recorded by the 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.On this base, 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 refuse this call outright –register()andregister_alias()testvalue’s membership for opposite outcomes, so neither can be the other’s implementation.AppType’s override differs: it mints a real member throughaenum.extend_enum, so its iteration grows too.- Parameters:
- Return type:
- 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 –aenum.extend_enumhas 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 does not grow the registry – contrastregister(), the explicit path that 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.