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: object

Bare 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. EnumRegistry adds the mutating half on top; a closed enumeration inherits this one directly and so is never handed a register it would have to refuse.

This is a plain mix-in rather than an aenum.Enum subclass, because an enumeration that already has members cannot be subclassed. Mixed in before the member type – class Foo(EnumRegistry, IntFlag), or class Bar(EnumLookup, IntEnum) – it contributes methods only, so aenum still resolves the member data type from the enumeration base: int for aenum.IntEnum and aenum.IntFlag, str for aenum.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 EnumRegistry leaves the member data type exactly where it was: class Foo(EnumRegistry, IntFlag) resolves as Foo -> EnumRegistry -> EnumLookup -> IntFlag -> int -> ..., so _member_type_ still comes from the enumeration base and not from anything in this module. Had this tier subclassed aenum.Enum in 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 both enum and aenum maintain, so nothing on this tier depends on aenum internals at all – the aenum.extend_enum call in this module belongs to EnumRegistry, which is the tier that mutates.

classmethod _validate_value(value)[source]

Hook: reject value if 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.const does, 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 None deliberately 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 deliberate get override 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 – EnumValueError is the fitting one and is already what the closed enumerations in pcapkit.protocols.internet.mh raise. Note what that buys on the get() path: EnumValueError subclasses ValueError, so a rejection here is caught by get()’s own except ValueError and falls back to default just as any other unresolvable value does. It is also quiet: get() always calls this hook inside a CaughtByCaller block for ValueError, so an override need not pass quiet=True for its rejection to log nothing there (#1845). An override raising something outside that hierarchy would instead propagate past default, unquieted, which is a real difference in behaviour rather than a stylistic preference. With no usable default, a rejection from this hook reaches the caller exactly as the override raised it – get() re-raises an in-library ValueError unchanged 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 before cls(key) – the one point at which a lookup can reach a subclass’s _missing_ and mint. The str-key path does not call it, because that path never calls cls(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 when register runs inside get()’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.register and AppType.register each call it at the same point.

EnumRegistry.register_alias() does not call it, and does not need to: it refuses any value that 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 phantom tests.test_docstring_contract.DocstringRaisesTests rejects. 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 key to the canonical member.

A shortcut for the [] operation, per the ruling on #842: given a name it is cls[key], and given a value it is cls(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; key may still mint through a _missing_ that GitHub issue #775’s ruling deliberately kept minting, on CGAType. Registering a member any other way is register()’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 outside CGAType, where such a value instead lands in both tables, exactly as register() would leave it – for a non-str key; the str case is qualified below. Both describe key resolution only. default never reaches _missing_ on either branch: a declared-but-unassigned default does not resolve to an unregistered member the way such a key does – it simply does not resolve, and the lookup error key itself would have raised propagates instead.

For a str key, 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, not cls(key): on a registry whose own _missing_ mints for an unrecognised value, routing a failed name lookup through the constructor would let a mere get() call mint a permanent member where it should just raise. Defensive rather than observed: none of the str-valued registries that reach this method (Command, FEATCode, Method, OptionType, AppType and its transport subclasses TCP, UDP, SCTP and DCCP, and TLSKeyLabel) mints on any path, so no live witness exists in this tree. CGAType, which still mints directly via aenum.extend_enum, is int-valued, so a str name 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 future str-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 of key to an already-registered value keeps that side non-minting on every str-valued registry, not only the ones without a minting _missing_; and default resolves through the same kind of _value2member_map_ lookup rather than cls(default) (#864), so for a str key every path through this method – name, value and default alike – 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 through cls(value) but not through get(value) when value is a str; the non-str path below has no such gap, since it always calls cls(key) and so always reaches _missing_. Closing that gap here would mean calling cls(key) for a str value too, which reopens the exact minting hazard the paragraph above exists to avoid – so the asymmetry is deliberate, not an oversight.

The non-str path calls _validate_value() immediately before cls(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 as EnumValueError – or any other ValueError subclass – is caught by the same except that catches an ordinary failed construction, and so falls back to default on the same terms; the str path does not call the hook, for the reason given on _validate_value() itself.

The hook and cls(key) run inside a CaughtByCaller block for ValueError, because every ValueError they raise is this method’s own lookup miss: caught here, then answered with default or raised to the caller as the miss. A loud EnumValueError an override constructs there is therefore quiet, though the override never passes quiet=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.exceptions rather than a builtin, per the ruling recorded on GitHub issue #923: in-library code raises from pcapkit.utilities.exceptions rather than a builtin, and whether ValueError or KeyError applies follows what stdlib’s Enum raises in the same circumstance: a name miss is KeyError-derived and a value miss ValueError-derived, matching E['nosuch'] and E(999) on a stdlib Enum, so every except KeyError and except ValueError around 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 default or 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 installed pcapkit’s. The name miss is EnumKeyError with quiet=True, because it is in-library control flow – in the get override of Method it 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 a logging.CRITICAL record on every such call – the GitHub issue #362 defect BaseError documents quiet for – and, outside development mode, install pcapkit’s exception hooks. EnumRegistry.register() refusing the same value stays loud.

  • An in-library rejection propagates unchanged. A ValueError that is already a BaseError – typically EnumValueError from 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. Only aenum’s and enum’s own “no member carries this value” is converted, into a quiet EnumValueError. This is the same discrimination EnumField.post_process already makes for the same reason.

Parameters:
  • key (Any) – Name or value to look up.

  • default (Any) – An already-registered value to fall back to when key does not resolve. Resolved through a plain _value2member_map_ lookup, never through cls(default), so it cannot mint – see #864. NO_DEFAULT stands for no default; that and a default naming no registered member both fall through to the same lookup error key itself would have raised.

Return type:

Self

Returns:

The canonical member for key, or for default.

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. A ValueError, so an except ValueError caller is unaffected. Quiet.

  • EnumKeyError – If a name does not resolve and there is no usable default. A KeyError, so an except KeyError caller is unaffected. Quiet.

Changed in version 1.5.0: A value miss is quiet, whether default answers it or it is raised: it logs nothing and installs no exception hook, while EnumRegistry.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, because get, get_all, register and register_alias are all to exist on every const registry (GitHub issue #842), and it is where a registry with genuinely several matches puts them: AppType overrides it to return every service IANA assigns to a port.

Parameters:

key (Any) – Name or value to look up.

Return type:

tuple[Self, ...]

Returns:

The canonical member, followed by any further distinct member carrying the same value.

Raises:
class pcapkit.corekit.enum.EnumRegistry[source]

Bases: EnumLookup

Registry protocol shared by every constant enumeration under pcapkit.const.

EnumLookup above 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 EnumLookup directly 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 gaining EnumLookup as 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 name the caller specifies, so nothing has to be guessed (GitHub issue #842). Contrast get() and _missing_, which resolve without naming anything.

Refuses a value that already has a member. Without this guard, aenum.extend_enum does not mint anything for an already-taken value – aenum treats that as a request to alias the existing member under the caller’s name instead, silently: the call returns the existing member, name becomes reachable in __members__ pointing at it, and _member_names_ does not grow. That is register_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 calling cls(value), for the same reason register_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: a value that already has a member is legal by construction, so the actionable “use register_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 a CaughtByCaller block declaring that nothing is caught – loud even if register is reached from inside a get (#1845).

Parameters:
  • value (Any) – Value of the new member.

  • name (str) – Name of the new member. Required rather than derived, which is the whole point – a generated name is a guess.

Return type:

Self

Returns:

The newly registered member.

Raises:
  • ValueError – If value already has a member – use register_alias() to add a further name for it instead.

  • EnumValueError – If a subclass’s _validate_value() rejects value. The base implementation of that hook accepts everything, so this cannot arise on a registry that does not override it.

  • ValueError – If name is already taken. aenum reports that as TypeError; it is translated so that the ways one call can fail are one exception type.

classmethod _extend(value, name)[source]

The raw aenum.extend_enum call, shared by register() and register_alias().

Neither public method calls the other: register() refuses an already-registered value before it would ever reach here, and register_alias() depends on the opposite of that – it verifies value is already registered and then relies on exactly the mint-or-alias behaviour this wraps to add name as 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 to register_alias() while register() still rejects it.

Parameters:
  • value (Any) – Value of the member, new or existing.

  • name (str) – Name to add.

Return type:

Self

Returns:

The member now reachable under name, new or existing.

Raises:

ValueError – If name is already taken. aenum reports that as TypeError; it is translated so that the ways one call can fail are one exception type.

classmethod register_alias(value, name)[source]

Add name as a further name for the member already at value.

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 whether AppType or 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 from AppType: 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 calling cls(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 than register(), which would refuse this call outright – register() and register_alias() test value’s membership for opposite outcomes, so neither can be the other’s implementation. AppType’s override differs: it mints a real member through aenum.extend_enum, so its iteration grows too.

Parameters:
  • value (Any) – Value of the existing member to alias.

  • name (str) – Alias to add for it.

Return type:

Self

Returns:

The existing member, now reachable under name as well.

Raises:

ValueError – If no member carries value, or if name is already taken.

classmethod register_aliases(value, *names)[source]

Add several aliases for the member at value, left to right.

Parameters:
  • value (Any) – Value of the existing member to alias.

  • *names (str) – Aliases to add for it.

Return type:

tuple[Self, ...]

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_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 does not grow the registry – contrast register(), the explicit path that does.

The member is constructed through cls._member_type_, which aenum sets from the enumeration base, so this serves int- and str-valued registries alike without either having to say which it is.

Parameters:
  • value (Any) – The member’s value.

  • name (str) – The member’s name.

Return type:

Self

Returns:

The unregistered member.