Minting an Unrecognised Value¶
Nearly every registry under pcapkit.const resolves an unrecognised value through
a _missing_ override, which decides what happens when a value has no member. Only
ExtensionHeader and
TLSKeyLabel have none anywhere in their
MRO, so they carry no declared-but-unassigned range to resolve. The rest define one
themselves, except the transport subclasses of
AppType, which inherit
AppType._missing_
(pcapkit/const/reg/apptype/apptype.py). The behaviours below follow from it, and
which one a given range gets is a design decision, not a style preference:
extend_enum(cls, name, value)– mintCreates a real, permanent member on the class. It is installed in
_member_map_and_value2member_map_, so it is visible to iteration, lookup and__members__from then on, for the life of the process._unregistered_member()– unmintReturns a member-like object for the value without installing it. The registry does not grow, and a second lookup of the same value builds an equal but distinct object rather than handing back the first.
super()._missing_(value)– hand back toaenumNeither of the above. For an
aenumflag base this composes a pseudo-member and caches it in_value2member_map_only, so__members__and iteration are unchanged but a second lookup returns the same object. For a plain enum it returnsNoneand the lookup raisesValueError; the measured case is an in-range hole in an unmint registry,BlockType(11).
Measured against the tree, by class:
Mints:
CGATypealone, whose values are 128-bit CGA extension tags rather than an IANA-style range of codes.Unmint: the bulk of the registries. The transport subclasses (
TCP,UDP,SCTP,DCCP) inherit the method; every other registry here calls_unregistered_memberitself, thoughAppTypenever gets there: it holds no members and raises for every value through itscls.__registry__ is Noneguard.Some of the rest can never reach the call either, because every range they declare is already covered by their own members –
ToSDelaydeclares0..1and has members[0, 1], so its_unregistered_membercall is dead code. Those found areipv4.option_class,ipv4.tos_del,ipv4.tos_ecn,ipv4.tos_pre,ipv4.tos_rel,ipv4.tos_thr,ipv6.option_action,ipv6.seed_id,ipv6.smf_dpd_mode,l2tp.type,mh.dhcp_support_modeandvlan.priority_level. Thestr-valuedftp.command.Command,ftp.command.FEATCodeandhttp.method.Methodwere not probed, so that list is a floor.Cache in the value table only:
Flagsand the Mobility Header flag registries, such asBindingACKFlag. They range-check and hand back tosuper()._missing_, which for anaenum.IntFlagreaches_create_pseudo_member_and ends incls._value2member_map_.setdefault(value, pseudo_member).Raise for any unknown value:
AppType,ExtensionHeader,TLSKeyLabelandTransport.Transportraises from its own range check: its declared range0..3is exactly its member set, so no unassigned value is left forsuper()._missing_(the line after the check is unreachable).
Note that these are aenum classes: issubclass(Flags, enum.IntFlag) is False
against the standard library and True against aenum.IntFlag, so a probe against
enum answers about the wrong hierarchy.
The mint criterion below therefore applies only to the registries that unmint, where it
decides the name an unregistered member carries – the registrar’s own label suffixed
with the individual code, or the bare block label on its own – not whether it
registers. In the others it never runs: CGAType names
its tag from the value, the flag registries compose a name from their members’ bits, and
those that raise name nothing. The crawlers that generate the ranges record that narrowing
in their own comments: pcapkit/vendor/reg/ethertype.py’s UNASSIGNED_ROW_NAMES
and pcapkit/vendor/ipx/socket.py’s UNASSIGNED_RANGE_NAMES.
---
config:
flowchart:
curve: monotoneX
---
flowchart LR
LOOKUP["cls(value) finds no member"] --> MISSING["\_missing\_"]
MISSING -->|"out of range, or no block covers it"| RAISE["ValueError"]
MISSING -->|"CGAType only"| MINT["extend_enum<br/>permanent member, registry grows"]
MISSING -->|"flag registries"| FLAG["super().\_missing\_<br/>pseudo-member cached in the value table only"]
MISSING -->|"block label names a party"| SUFFIXED["_unregistered_member<br/>label + _0x<code>"]
MISSING -->|"block label names a procedure"| BARE["_unregistered_member<br/>bare block label"]
The Criterion¶
The test, paraphrased from the maintainer’s ruling: does the upstream registry treat the label as the final, concrete assigned name, or only as a notation for a human reading the table?
Settled in review of the
Socket._missing_ branch-order fix
(#841) and reaffirmed on #775 as a core concept of the ruling.
So the question to ask of a range is what the upstream registry actually did, not what the generated code happens to look like:
The source assigns a real, specific name to those codes. The name records something the registry genuinely says, so it is carried across and suffixed with the code it belongs to.
The source says only that the codes are spoken for, without naming them –
Unassigned,Reserved,Reserved for Private Use,Reserved for Experimental Use,Deprecated,Dynamically AssignedandStatically Assigned. These are written for a human reading the table, so the bare label stands: suffixing one with a code would manufacture a name nobody assigned, and the value will get its real name if and when something assigns it.
Worked Examples¶
Bare label. pcapkit.const.ipx.socket’s Dynamically Assigned,
Dynamically Assigned Socket Numbers, Statically Assigned Socket Numbers and
Experimental ranges. Each names how the socket will be allocated, not what
occupies it; the real name arrives with the allocation.
Suffixed. pcapkit.const.reg.ethertype’s company names – Xyplex,
Datability, Qualcomm, Motorola and many others, some of them holding more
than one range block – and
pcapkit.const.ipx.socket’s Registered by Xerox. A company name is the
assignment, for the reason in the next section.
Further ranges in that file carry a suffixed name without being company names at
all: IEEE802.3 Length Field, which names a field in a standard, and
Berkeley Trailer encap/IP, which names an encapsulation. Neither is among the company names, and
neither is suffixed for the reason the next section gives.
Note
Those two groups look alike and the line between them is not range-versus-single
code. Registered by Xerox covers a range and is still suffixed per socket,
because it names who registered the socket. Dynamically Assigned also covers a
range and is not, because it names only the mechanism by which some future party
will take it. Ask what the label tells you: a party, or a procedure.
Suffixed Company Names¶
The ethertype case looks like an exception to the rule and is not. The maintainer’s
reasoning, settled on #775 after
being raised in review of the same
Socket._missing_ fix (#841): a
proprietary protocol will never have a public name, so the company name is what serves
that purpose in its place.
So the company name is not a note about the code – it is the best name that will ever
exist for it, which makes it the final concrete assigned name under the test above.
That is also why DEC Unassigned goes the other way despite carrying the same
attribution: there the company holds the block and assigned nothing, so the notation is
“Unassigned” and the attribution is incidental.
Checking the Current State¶
The split is measurable rather than a matter of memory. Slice each _missing_ body
and see which call it makes – an ast walk is reliable where a text search is
not, because extend_enum also appears in imports and in prose. Match on the
attribute name as well as on the bare name: extend_enum is called as a plain
function, but _unregistered_member is called on cls, so a walk that inspects
only ast.Name nodes finds every mint and no unmint at all:
import ast, pathlib
for path in sorted(pathlib.Path('pcapkit/const').rglob('*.py')):
if path.name == '__init__.py':
continue
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == '_missing_':
calls = {n.func.id if isinstance(n.func, ast.Name) else n.func.attr
for n in ast.walk(node) if isinstance(n, ast.Call)
and isinstance(n.func, (ast.Name, ast.Attribute))}
if 'extend_enum' in calls:
print('MINT ', path)
elif '_unregistered_member' in calls:
print('UNMINT', path)
Note
The walk reads each file’s own _missing_, so it reports
AppType as an unmint although it raises,
prints nothing for the flag registries and
Transport (they call neither function) nor for
ExtensionHeader and
TLSKeyLabel (they define no
_missing_), and does not see the transport subclasses, which inherit theirs.
Reconcile it against a runtime probe for the split in the list above.
Warning
Calling Cls(value) on a registry that mints or hands back to a flag base
mutates the class. A probe is not a read: it installs something that every later
lookup then finds. CGAType
grows __members__, _member_map_ and _value2member_map_;
Flags and the Mobility Header flag registries
grow _value2member_map_ only. Snapshot {member.value for member in Cls}
before any lookup regardless – that snapshot cannot see the flag registries’ cache,
so compare len(Cls._value2member_map_) too – and use a throwaway process per
registry when comparing behaviour across revisions.