Minting an Unrecognised Value

A registry under pcapkit.const resolves a value with no member through a _missing_ override. Only ExtensionHeader and TLSKeyLabel have none anywhere in their MRO, so they carry no declared-but-unassigned range. The rest define their own, except the transport subclasses of AppType, which inherit AppType._missing_. Which of these behaviours a range gets is a design decision, not a style preference:

extend_enum(cls, name, value) – mint

Creates a permanent member, installed in _member_map_ and _value2member_map_, so iteration, lookup and __members__ see it for the life of the process.

_unregistered_member() – unmint

Returns a member-like object without installing it. The registry does not grow, and a second lookup builds an equal but distinct object.

super()._missing_(value) – hand back to aenum

For an aenum flag 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 returns None and the lookup raises ValueError; the measured case is an in-range hole in an unmint registry, BlockType(11).

Measured against the tree, by class:

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.

So the mint criterion below applies only to the registries that unmint, where it decides the name an unregistered member carries – the registrar’s label suffixed with the code, or the bare block label – not whether it registers. Elsewhere 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 this in their comments: pcapkit/vendor/reg/ethertype.py’s UNASSIGNED_ROW_NAMES and pcapkit/vendor/ipx/socket.py’s UNASSIGNED_RANGE_NAMES.

        ---
config:
  flowchart:
    curve: monotoneX
    nodeSpacing: 30
    wrappingWidth: 150
---
flowchart TD
    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&lt;code&gt;"]
    MISSING -->|"block label names a procedure"| BARE["\_unregistered\_member<br/>bare block label"]
    RAISE ~~~ MINT ~~~ FLAG ~~~ SUFFIXED ~~~ BARE
    

The Criterion

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 ask what the upstream registry actually did, not what the generated code looks like:

  • The source assigns a real, specific name to those codes, so it is carried across and suffixed with each code.

  • The source says only that the codes are spoken for – Unassigned, Reserved, Reserved for Private Use, Reserved for Experimental Use, Deprecated, Dynamically Assigned and Statically Assigned. These are notation for a human reader, so the bare label stands: suffixing it would manufacture a name nobody assigned, and the value gets its real name when something assigns it.

Note

In the transport registries – TCP, UDP and the rest – the spans IANA assigns to a service are an exception to the first case: x11, ircu, swx and the others resolve with the bare service name, so TCP(6001).name is 'x11', not 'x11_6001'. Suffixing them would change .name, which a caller can observe. Recorded on #2550.

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.

Suffixed. pcapkit.const.reg.ethertype’s company names – Xyplex, Datability, Qualcomm, Motorola and others, some holding more than one range block – and pcapkit.const.ipx.socket’s Registered by Xerox. A company name is the assignment, as the next section explains.

Other suffixed ranges in pcapkit.const.reg.ethertype are not company names: IEEE802.3 Length Field names a field in a standard and Berkeley Trailer encap/IP an encapsulation, so the next section’s reason does not cover them.

Note

The line between the two groups is not range versus single code. Registered by Xerox covers a range and is suffixed per socket, because it names who registered the socket. Dynamically Assigned covers a range and is not, because it names only the mechanism by which some future party will take it. Ask whether the label names a party or a procedure.

Suffixed Company Names

The ethertype case is not an exception. 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 serves in its place. It is not a note about the code but the best name that will ever exist for it, which makes it the final concrete assigned name under the test above.

DEC Unassigned goes the other way despite the same attribution: the company holds the block and assigned nothing, so the notation is “Unassigned” and the attribution is incidental.

Checking the Current State

Measure the split: walk each _missing_ body with ast for the call it makes. A text search is unreliable, since extend_enum also appears in imports and prose. Match the attribute name as well as the bare name: extend_enum is a plain function but _unregistered_member is called on cls, so a walk of ast.Name nodes alone finds no unmint:

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, Transport, Type and ManagementSubtype (they call neither function) nor for ExtensionHeader and TLSKeyLabel (they define no _missing_), and does not see the transport subclasses, which inherit theirs. CommandType is a flag registry; the UNMINT lines for pcapkit/const/ftp/command.py are the file’s other registries. Reconcile the walk against a runtime probe for the split above.

Warning

Calling Cls(value) on a registry that mints or hands back to a flag base mutates the class: every later lookup finds what the probe installed. CGAType grows __members__, _member_map_ and _value2member_map_; Flags, CommandType and the Mobility Header and radiotap 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.