Where the registry protocol lives¶
get(),
get_all(),
register() and
register_alias() are expected to exist on
every registry, per the ruling on
#842. They come from
EnumRegistry, mixed in ahead of the enum base so that
_member_type_ still resolves to int or str:
class LinkType(EnumRegistry, IntEnum):
...
A handful of registries define their own __new__ to carry extra attributes and so
do not share the generated template; bringing them onto the base is tracked in
#860.
The Two Tiers, and What Lives on Each¶
EnumRegistry is not the only base any more. Since
phase 1 of #877 it has a
parent, and the line between them is whether the enumeration may grow:
|
|
|
The owner’s ruling, verbatim: “they may subclass a bare base enum from
pcapkit.corekit.enum - where EnumRegistry subclasses it for using in the other
mutable ones.” So a closed set inherits EnumLookup
directly and is never handed a register it would have to refuse; an open
registry inherits EnumRegistry exactly as before.
What settled the split is the owner’s own second thought about carrying register
on the base: “if it carries ``register``, then why not ``register_alias``. We might
be creating a bad ruling.” Following that through leaves
EnumRegistry holding only three methods, too thin to
justify a second class – so the two tiers collapse into one, which is the opposite of
what was ruled.
Both tiers are plain classes, and that is load-bearing. Inserting a parent above
EnumRegistry leaves the member data type exactly where
it was – LinkType -> EnumRegistry -> EnumLookup -> IntEnum -> int – so
_member_type_ still comes from the enum base. Had either tier subclassed
aenum.Enum in order to “be an enum”, it would have become the member type
itself and broken int, str and flag registries at once.
_validate_value() is what the base carries
instead of register, and it answers the owner’s other requirement: “there must
be some sort of range validation logic for the inherited classes to hook in.” The
base implementation accepts everything; an override states a range, in the shape the
generated registries currently spell by hand in _missing_:
@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__}')
Three things about it are easy to get wrong:
It guards, it does not convert. The return type is
Nonedeliberately, so that an override cannot normalise a value on its way through and silently change what a lookup resolves to.Raise from
pcapkit.utilities.exceptions.EnumValueErroris the fitting one, and because it subclassesValueErrora rejection is caught byget’s ownexceptand falls back todefaultlike any other unresolvable value. An override raising outside that hierarchy propagates pastdefaultinstead.With no usable
default, the rejection reaches the caller unwrapped.getre-raises aValueErrorthat is already aBaseErrorexactly as the override raised it, and converts onlyaenum’s andenum’s own “no member carries this value”. Two things follow, and both are the point of the discrimination rather than side effects: the override’s own message survives to the caller instead of being replaced by the base’s, and the error is logged once rather than twice, sinceBaseErrorlogs on construction and re-wrapping would construct a second one. So an override should say in its message what it rejected and why; that message is what the caller sees.A
strkey never reaches it. That path never callscls(key), so it resolves only against already-populated lookup tables, where every value present is legal by construction.registerdoes call it, after its duplicate check.
Note
Re-parenting every non-registry enumeration onto
EnumLookup was phase 2 of
#877, and it is now
complete: the phase landed for 24 of the 24 non-registry enumerations.
Zero enumerations remain outside the hierarchy, measured by the same runtime
walk over both the enum and aenum flavours that once found seven.
It landed in two pull requests rather than one. Seven of the 24 sat in files
other pull requests were editing around the same time: CommandType and
ConformanceRequirement in pcapkit.const.ftp.command and its vendor
template, both touched by #913;
and ESPStatus in pcapkit.protocols.internet.esp plus all four
pcapkit.protocols.internet.mh helpers (FastBindingAcknowledgmentStatus,
IPv6AddressPrefixCode, LMAAddressCode, LocalizedRoutingStatus), both
files touched by #924. The
first pass (#921) is
behaviour-preserving on its own, so the other 17 could land without waiting on
those files; the remaining seven followed once both had merged
(#930).
What a Failed Lookup Raises¶
Two rules govern it, and they pull in opposite directions on purpose. The owner’s ruling, verbatim, on #923:
Either
ValueErrororKeyError, that’s depending on how stdlib’sEnumwould raise on these circumstances. And we should raise one frompcapkit.utilities.exceptionsrather builtin exceptions.
So the provenance is in-library and the shape is stdlib’s:
get()raisesEnumKeyErrorfor a name miss andEnumValueErrorfor a value miss, both frompcapkit.utilities.exceptionsrather than from builtins.The split between the two is not taste.
E['nosuch']raisesKeyErrorandE(999)raisesValueErroron a stdlibEnum, so a miss by name isKeyError-derived here and a miss by value isValueError-derived, matching it.That is what keeps the ruling cheap to carry out:
EnumKeyErrorderivesKeyErrorandEnumValueErrorderivesValueError, so only the provenance changed – everyexcept KeyErrorandexcept ValueErroraround a lookup keeps catching, in this tree and in a caller’s.
Do not “improve” on the shape by making both misses report identically. Converting
one into the other is exactly what #923 retired, and it was retired in three places
at once: TransportProtocol.get and Criticality.get had each turned the
base’s KeyError into a ValueError, and
FastBindingAcknowledgmentStatus.get raised
EnumValueError for a name miss so that “the
two ways of getting it wrong reported identically”.
One asymmetry between the two is deliberate and is not visible from the
exception class: the name miss is raised quietly
(BaseError’s quiet=True, so nothing is
logged and sys.tracebacklimit is left alone) while the value miss stays
loud. A name miss is in-library control flow at several call sites, and at
Method.get it is part of a successful call –
that override catches it in order to mint. A loud error there would put a
logging.CRITICAL record on every such call and set
sys.tracebacklimit to 0 process-wide, which is the
#362 defect quiet
exists for. So a get override that catches a name miss as control flow is
following the convention; one that catches a value miss that way is silencing a
logged error, and needs a reason.
Case Sensitivity Is RFC-Directed¶
The rule, in the owner’s own wording on #877:
if RFC states the values are case-insensitive, then our enum should also treat them that way. otherwise, we should treat them case sensitive.
And the reason a registry’s spelling is never quietly normalised, from the same thread: “enum should honour and keep their original writings as in the registrars. case in-sensitivity only applies to certain selected ones, where logically it makes sense (like ``TransportProtocol``) and/or RFC documentation itself recognises them as case-insensitive (like, maybe, FTP/HTTP commands).”
So get() is case-sensitive, and that is the
default every enumeration gets. Case-insensitivity is a per-class get override that
has to cite the RFC or IANA registry making the values case-insensitive; without one it
is a defect rather than a convenience. That also means no public member is ever renamed
to make a lookup work – which is what keeps
Parameter’s R1_Counter = 128 and
R1_COUNTER = 129, two IANA-registered HIP parameters differing only in case, both
resolvable.
And a folding override carries only the fold. TransportProtocol.get is the
worked example: since
#923 it lowers key,
forwards default verbatim and delegates to super().get(), and that is all it
does. It used to convert the base’s name-miss KeyError into a
ValueError as well, and #923’s ruling retired that; the
#836 refusal to extend the
class at all is untouched by the retirement, since only the exception class moved.
Criticality.get went further and no longer exists: conversion was the only
thing it added over the base, so once that went there was nothing left for an
override to hold, and the class inherits
get() unchanged. An override that would
now be empty is deleted, not kept as a pass-through – a get that only calls
super().get() reads as though it were doing something, and the next reader has
to diff it against the base to find out that it is not.
The Lenient Criterion, in Two Limbs¶
The ruling above leaves one question open, and #903 settled it: does a specification have to state a comparison rule for a registry to be treated case-insensitively, or does it also count when the authorities merely disagree about spelling? The owner’s answer, verbatim:
I say lenient. TransportProtocol for example should be case-insensitive. Upper or lower cases are being used everywhere in RFC and IANA themselves so that’s an indication of case insensitivity.
So the test a new registry has to pass has two limbs, and satisfying either one justifies case-insensitivity:
A comparison rule in the governing document. RFC 959 Section 4.1 for FTP command codes, RFC 5797 Section 2 for FTP FEAT codes, RFC 6335 Section 5.1 for IANA service names.
A documented spelling disagreement between the specification and the registry. If the RFC writes a field one way throughout and the live IANA data writes it another, then neither authority is treating case as significant, and a lookup that does would reject a caller holding the spec’s own spelling.
Limb 2 has to be measured, not assumed – count the casings in the registry the crawler actually reads, and say how many rows carried each. A guess about which way IANA spells a column is not evidence.
Where neither limb holds, the lookup is case-sensitive and inherits
get() unchanged.
The Audit, per Class¶
#903’s sweep, so that a registry added later has something to check itself against. The owner’s scope for it, verbatim: “we should audit all registries and then decide if case (in)sensitive.”
The population it covers, with the counting convention spelled out because the
figures move: 127 EnumRegistry subclasses, every
one of them under pcapkit.const, across 124 files – 117 int-valued
(of which 5 are flag registries) and 10 aenum.StrEnum-valued. Plus 24
non-registry enumerations counted by a runtime walk over both the enum and
aenum flavours and including nested classes: 17 top level (3 of them under
pcapkit.const itself) and 7 nested, the nested ones being
FrameType.Flags in pcapkit.protocols.schema.application.httpv2 plus its
6 concrete per-frame subclasses. 151 enumerations in total.
The int-valued tier, all 117, is case-sensitive, and the criterion is
vacuous on it rather than merely unmet. A registry whose values are numbers has
nothing for case to apply to; the only way a string reaches
get() on one is as a member name, and a name
is the Python identifier pcapkit.vendor.default.Vendor.safe_name() derives from
the registry’s own name column – it preserves the registrar’s casing exactly, but it
is not itself a value any specification states a comparison rule for. Measured across
all 151 enumerations: exactly one would collide if names were folded –
Parameter, on R1_Counter against
R1_COUNTER – and no enumeration anywhere has two str values that
collide when folded. So folding names is not merely unjustified, it is unsafe in a
measured case; folding values is safe but unjustified except where the table below
says otherwise.
That leaves the classes with something to decide:
Class |
Governing source |
What it says |
Verdict |
|---|---|---|---|
“Upper and lower case alphabetic characters are to be treated identically.” Limb 1. |
case-insensitive – |
||
“IANA maintains uniqueness of feature names (FEAT codes) based on
case-insensitive comparison.” Limb 1. Limb 2 holds too: RFC 2389 recommends
upper case on the wire while the registry spells 5 of its 15 codes lower case
(measured: of 64 rows, 11 upper-case / 10 distinct, 52 lower-case / 5
distinct, 1 blank, 0 mixed). Read §3.2 to the end before concluding it
disagrees: it opens by calling the feature-label “nominally case
sensitive”, then defers to “the definitions of specific labels”, which
RFC 5797 §2 above is. Note also that the §2 sentence is wrapped across a
line break in the RFC’s text file, at |
case-insensitive – was a defect; |
||
“The method token is case-sensitive.” Explicitly the opposite of limb 1. |
case-sensitive – was a defect, fixed by #896 |
||
|
Nothing states a rule; the draft never discusses option-name case. |
case-sensitive – already exact-matches, conforms |
|
Nothing states a rule. Limb 2 fails on measurement: all 10 rows of the RFC’s table and all 10 of the live IANA CSV are upper case, so the authorities agree. (RFC 9850 notes the labels “correspond to lowercase labels in the TLS key schedule”, but those are a different document’s secret names, not a second spelling of the log label.) |
case-sensitive – no override, conforms |
||
|
Nothing states a rule for the |
case-insensitive – |
|
– |
Moot: its |
n/a – int-keyed |
|
|
“case is ignored for comparison purposes, so both “http” and “HTTP” denote the same service.” Limb 1, emphatically – and these registries’ values are service names. |
unimplemented – no service-name lookup exists to fold; see below |
|
|
Limb 2 holds on measurement: the RFC and registry pages present the kind and
conformance letters upper case ( |
open – see below |
|
IANA Mobility Header registries, 3GPP TS 38.413 |
Their values are numeric codes, so the criterion is vacuous exactly as for the
|
case-sensitive – conforms |
|
|
|
The draft names the four labels outright (“The key type is one of
LOCAL_STATIC_PRIVATE_KEY, …”) and, as for |
case-sensitive – no override, conforms |
Every other non-registry enumeration |
– |
pcapkit’s own discriminators and bit labels, with no registrar behind them
at all – |
case-sensitive – nothing to cite, nothing to change |
Two rows the audit deliberately left open rather than acting on, because each is wider than a case fix:
A service-name lookup on the
AppTypetransport registries. This is the inverse of every other row: RFC 6335 Section 5.1 does make service names case-insensitive, andTCP/UDP/SCTP/DCCPhold service names as their values – butAppType.getrefuses a non-intkey, so no service-name lookup exists for the rule to apply to. Implementing one is new public API on a 6,000-member registry where one name maps to many ports, which is aget_alldesign question rather than a case fold.CommandTypeandConformanceRequirement. By parity withTransportProtocol– anint-valued enumeration whose names are the specification’s own tokens – the measured spelling disagreement above would make these two case-insensitive. Nothing looks them up by string today, though: the crawler translates the CSV’s lower-case letters to the upper-case member names at generation time. Both classes now inheritEnumLookup– #930 finished re-parenting them, per the note above – so agetexists on each, case-sensitive like the base’s own. Whether to fold case to matchTransportProtocol’s own override is a design question for whoever writes the first string-keyed caller, not one this audit settles.
One case fold also lives outside any get, and so escapes this convention
entirely: _resolve in pcapkit.protocols.internet.esp upper-cases its
value before matching it against Cipher and
Integrity member names. RFC 7296 states no
comparison rule for IKEv2 transform names – checked, it does not discuss case at all
– so that fold is a convenience with no citation behind it. It is a protocol-level
resolver rather than a registry override, which is why the audit records it here
rather than changing it.
Note
The obstacle this page used to record – that the base’s string-key path does not
fall through to a value lookup, so an aenum.StrEnum registry would stop
resolving a valid value that is not also a name – no longer applies.
get() now checks _value2member_map_
when the name lookup misses, so such a value resolves:
>>> FEATCode['base'].value
'<base>'
>>> '<base>' in FEATCode._member_map_
False
>>> FEATCode.get('<base>')
<FEATCode [base]>
The example above is FEATCode’s shape, and it
still resolves exactly as shown – but since
#903 that class overrides
get too, so the output is only the base’s because its override delegates an
exact name-or-value hit straight through. Measure the base on a registry that does
not override get at all. Five do –
Command,
FEATCode,
Method,
OptionType and
AppType – and probing one of those
measures the override rather than the base. The unconditionally clean witness is
tests/corekit/test_enum_lookup_base_unit.py’s own _Str, a purpose-built
closed set carrying angled = '<angled>' precisely so that the value
fall-through can be measured on a class that defines no get.
Command.get upper-cases its key
before matching, which makes it look as though the base were case-insensitive –
deliberately, since RFC 959 Section 4.1 treats FTP command codes identically
regardless of case. Method.get used to fold case the same way, but
#896 made it
case-sensitive instead: RFC 9110 Section 9.1 says the HTTP method token is
case-sensitive, so Method.get('get') no longer resolves to
Method.GET – it builds its own unregistered member, preserving the
caller’s exact casing, the same way an unrecognised value always does.
What does survive is narrower and deliberate: a declared-but-unassigned str
value resolves through cls(value) but not through get(value), because
_unregistered_member() returns it without
growing either lookup table. FEATCode.get('ZZ-NOT-REAL') raises
EnumKeyError – which is a KeyError, so
an except KeyError around it is unaffected – while
FEATCode('ZZ-NOT-REAL') yields an unregistered member. Closing that gap would
mean calling cls(key) for a str value too, which reopens the minting hazard
above – so the asymmetry is intended, and get’s own docstring carries the full
reasoning.
See also
pcapkit.vendor generates these modules. A change to the shape of a
generated registry belongs in the crawler or in
pcapkit.vendor.default’s template, never in the generated file alone –
the next regeneration would discard it.