Source code for pcapkit.corekit.enum

# -*- coding: utf-8 -*-
"""Constant Enumeration Base
==============================

.. module:: pcapkit.corekit.enum

:mod:`pcapkit.corekit.enum` contains the bases every enumeration in this
library is meant to inherit from, split by whether the enumeration may *grow*:

* :class:`EnumLookup` -- the bare **lookup** half: :meth:`~EnumLookup.get`,
  :meth:`~EnumLookup.get_all` and the overridable
  :meth:`~EnumLookup._validate_value` guard. Nothing here mutates the
  enumeration, so it is what a *closed* set can inherit without being handed a
  contract it must refuse.
* :class:`EnumRegistry`, a subclass of the above -- adds the **mutating** half:
  :meth:`~EnumRegistry.register`, :meth:`~EnumRegistry.register_alias`,
  :meth:`~EnumRegistry.register_aliases`, :meth:`~EnumRegistry._extend` and
  :meth:`~EnumRegistry._unregistered_member`. Every generated registry under
  :mod:`pcapkit.const` inherits from here.

That split follows the rules laid down in GitHub issue :issue:`877`: a helper
enumeration is immutable by default, unless RFC or IANA documents its value space as
open. Such closed sets subclass a bare base enumeration in this module, and
:class:`EnumRegistry` subclasses that base for the mutable ones.

Which methods land on which tier was settled in the same thread. The deciding
consideration was that a base carrying ``register`` has no principled reason to
withhold ``register_alias``, so drawing the line between them risked setting a
bad precedent. Following that through,
a base holding both would leave :class:`EnumRegistry` with only
``register_aliases``, ``_extend`` and ``_unregistered_member`` -- too thin to
justify a second class, collapsing the two tiers into one. So every mutating
method stays put, and what the base carries instead is the other requirement
from that thread: some range-validation logic that inheriting classes can hook
into, which is :meth:`EnumLookup._validate_value`. Legality is
every enumeration's concern; mutation is only the open registries'.

Supporting measurement, taken when :issue:`877` split the tiers: **none** of
the non-registry enumerations defines ``register``, ``register_alias``,
``register_aliases`` or ``_unregistered_member``, and **no**
:class:`EnumRegistry` subclass exists outside :mod:`pcapkit.const`. The mutating half
therefore had no users to serve among the classes being re-parented.

The registry tier's own shape is the earlier design settled in GitHub issue
:issue:`842`: ``get``, ``get_all``, ``register`` and ``register_alias`` are to
exist on every const registry, so the abstraction is finished by moving them to
the base class. The closed sets split off by :issue:`877` are outside that
contract. :class:`~pcapkit.const.reg.apptype.apptype.AppType`'s sub-base class carries the overrides and dispatching
logic it needs, and :class:`~pcapkit.const.reg.apptype.apptype.AppType`'s subclasses override again
where their contracts differ.

That is a tiered hierarchy, of which this module is **tier one**:

1. :class:`EnumRegistry` -- ``get``, ``get_all``, ``register`` and
   ``register_alias``, in the form that suits a registry mapping one key to one
   member. ``register`` and ``register_alias`` are its own; ``get`` and
   ``get_all`` are declared one level up on :class:`EnumLookup` and reach it by
   inheritance. So a registry under :mod:`pcapkit.const` gets all of them from
   this module, apart from the
   overrides below and a few hand-written ``get`` overrides.
2. :class:`~pcapkit.const.reg.apptype.apptype.AppType`'s sub-base -- overrides each of them, routing the lookups
   through its ``_dispatch``, because a port lookup needs a transport protocol
   to be answerable at all. Defined not in this module but in
   :class:`pcapkit.const.reg.apptype.apptype.AppType` itself (GitHub issue
   :issue:`860`), which mixes in :class:`EnumRegistry` directly. ``register`` and
   ``register_alias`` are overridden without that dispatch, staying on the
   registry they are called on, so that a write never leaks onto a transport
   IANA never assigned the service to -- a minted member for the first, an
   alias for the second. Plus ``_unregistered_member``, for its own extra
   attributes (``svc``, ``port``, ``proto``) that the generic one below does
   not know to set.
3. The :class:`~pcapkit.const.reg.apptype.apptype.AppType` transport subclasses --
   :class:`~pcapkit.const.reg.apptype.tcp.TCP`, :class:`~pcapkit.const.reg.apptype.udp.UDP`,
   :class:`~pcapkit.const.reg.apptype.sctp.SCTP`, :class:`~pcapkit.const.reg.apptype.dccp.DCCP` --
   need no override of their own at all: ``_dispatch`` already returns ``cls`` unchanged the moment
   ``cls.__registry__`` is not :obj:`None`, which is true for exactly these subclasses, so tier 2's
   lookups already answer correctly on each of them without a further layer.

Defining these methods here, rather than as generated *text* written out
longhand in :data:`pcapkit.vendor.default.LINE` and copied into each crawler
that replaces that template wholesale, is what :issue:`775` asks for: as text,
adding one method means editing every bespoke template by hand.

The contracts are those set out on GitHub issue :issue:`842`: ``get`` is a
shortcut for the ``[]`` operation and returns the canonical enumeration member;
``get_all`` returns every matching member; ``register`` mints a new member on
the class at runtime under the name the caller specifies, so nothing has to be
guessed; ``register_alias`` (and ``register_aliases``) adds further alias names
to a given member's mapping.

.. versionadded:: 1.5.0

   The ``get`` each :mod:`pcapkit.const` enumeration carried as generated text
   moves onto :class:`EnumLookup`, and :class:`EnumRegistry` adds ``register``,
   which is how a member is minted at runtime: outside :class:`~pcapkit.const.mh.cga_type.CGAType`, looking up
   an unknown value does not mint one (:issue:`775`).

"""
from typing import TYPE_CHECKING

from aenum import extend_enum

from pcapkit.corekit.sentinels import NO_DEFAULT
from pcapkit.utilities.exceptions import BaseError, CaughtByCaller, EnumKeyError, EnumValueError

if TYPE_CHECKING:
    from typing import Any

    from typing_extensions import Self

__all__ = ['EnumLookup', 'EnumRegistry']


[docs] class EnumLookup: """Bare lookup protocol, shared by open registries and closed sets alike. Carries :meth:`get`, :meth:`get_all` and the :meth:`_validate_value` guard -- everything an enumeration needs in order to be *read* by name or by value, and nothing that could grow it. :class:`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* :class:`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 :mod:`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 :class:`EnumRegistry`, which is the tier that mutates. """ if TYPE_CHECKING: #: The enumeration machinery's own lookup tables and member data type, #: declared here because they are contributed by the ``aenum.Enum`` #: base this mix-in is combined with rather than by the mix-in itself. _member_map_: 'dict[str, Self]' _member_names_: 'list[str]' _value2member_map_: 'dict[Any, Self]' # NOTE: deliberately ``Any`` rather than ``type``. Annotating the member # data type precisely makes ``cls._member_type_.__new__(cls, value)`` # resolve to ``type.__new__``, which mypy then reads as building a # *class* rather than an instance -- mypy errors for a call that is # correct. Which concrete type it is depends on the Enum base each # subclass picks, so there is nothing more precise to say here anyway. _member_type_: 'Any'
[docs] @classmethod def _validate_value(cls, value: 'Any') -> 'None': """Hook: reject ``value`` if this enumeration's contract does not allow it. GitHub issue :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 :mod:`pcapkit.const` does, its vendor generator emitting the check its ``_missing_`` makes (:issue:`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 :obj:`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 :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 :mod:`pcapkit.utilities.exceptions`, per the same issue's ruling that in-library code raises in-library exceptions -- :exc:`~pcapkit.utilities.exceptions.EnumValueError` is the fitting one and is already what the closed enumerations in :mod:`pcapkit.protocols.internet.mh` raise. Note what that buys on the :meth:`get` path: :exc:`~pcapkit.utilities.exceptions.EnumValueError` subclasses :exc:`ValueError`, so a rejection here is caught by :meth:`get`'s own ``except ValueError`` and falls back to ``default`` just as any other unresolvable value does. It is also quiet: :meth:`get` always calls this hook inside a :class:`~pcapkit.utilities.exceptions.CaughtByCaller` block for :exc:`ValueError`, so an override need not pass ``quiet=True`` for its rejection to log nothing there (:issue:`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 -- :meth:`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: * :meth:`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. * :meth:`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 :meth:`get`'s quiet block, since a refused mint is a fault to report rather than a lookup that missed. Of the registries that override :meth:`EnumRegistry.register` to mint through a path of their own, :meth:`OptionType.register <pcapkit.const.pcapng.option_type.OptionType.register>` and :meth:`AppType.register <pcapkit.const.reg.apptype.apptype.AppType.register>` each call it at the same point. :meth:`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. :meth:`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 :class:`tests.test_docstring_contract.DocstringRaisesTests` rejects. An override adds its own clause naming what *it* rejects. Args: value: 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. """
[docs] @classmethod def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'Self': """Resolve ``key`` to the canonical member. A shortcut for the ``[]`` operation, per the ruling on :issue:`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 :issue:`775`'s ruling deliberately kept minting, on :class:`~pcapkit.const.mh.cga_type.CGAType`. Registering a member any other way is :meth:`~EnumRegistry.register`'s job and nobody else's, which is the ruling :issue:`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 :meth:`~EnumRegistry._unregistered_member`, to a member that is deliberately absent from the lookup tables -- true outside :class:`~pcapkit.const.mh.cga_type.CGAType`, where such a value instead lands in *both* tables, exactly as :meth:`~EnumRegistry.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 (:class:`~pcapkit.const.ftp.command.Command`, :class:`~pcapkit.const.ftp.command.FEATCode`, :class:`~pcapkit.const.http.method.Method`, :class:`~pcapkit.const.pcapng.option_type.OptionType`, :class:`~pcapkit.const.reg.apptype.apptype.AppType` and its transport subclasses :class:`~pcapkit.const.reg.apptype.tcp.TCP`, :class:`~pcapkit.const.reg.apptype.udp.UDP`, :class:`~pcapkit.const.reg.apptype.sctp.SCTP` and :class:`~pcapkit.const.reg.apptype.dccp.DCCP`, and :class:`~pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`) mints on any path, so no live witness exists in this tree. :class:`~pcapkit.const.mh.cga_type.CGAType`, which still mints directly via ``aenum.extend_enum``, is :class:`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)`` (:issue:`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 :meth:`~EnumRegistry._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 :meth:`_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 :exc:`~pcapkit.utilities.exceptions.EnumValueError` -- or any other :exc:`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 :meth:`_validate_value` itself. The hook and ``cls(key)`` run inside a :class:`~pcapkit.utilities.exceptions.CaughtByCaller` block for :exc:`ValueError`, because every :exc:`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 :exc:`~pcapkit.utilities.exceptions.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 :mod:`pcapkit.utilities.exceptions` rather than a builtin, per the ruling recorded on GitHub issue :issue:`923`: in-library code raises from :mod:`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 :exc:`KeyError`-derived and a value miss :exc:`ValueError`-derived, matching ``E['nosuch']`` and ``E(999)`` on a stdlib :class:`~enum.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 :mod:`pcapkit`'s. The name miss is :exc:`~pcapkit.utilities.exceptions.EnumKeyError` with ``quiet=True``, because it is in-library control flow -- in the ``get`` override of :class:`~pcapkit.const.http.method.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 :issue:`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 :data:`logging.CRITICAL` record on every such call -- the GitHub issue :issue:`362` defect :class:`~pcapkit.utilities.exceptions.BaseError` documents ``quiet`` for -- and, outside development mode, install :mod:`pcapkit`'s exception hooks. :meth:`EnumRegistry.register` refusing the same value stays loud. * **An in-library rejection propagates unchanged.** A ``ValueError`` that is already a :exc:`~pcapkit.utilities.exceptions.BaseError` -- typically :exc:`~pcapkit.utilities.exceptions.EnumValueError` from a subclass's :meth:`_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 :mod:`enum`'s own "no member carries this value" is converted, into a quiet :exc:`~pcapkit.utilities.exceptions.EnumValueError`. This is the same discrimination :meth:`EnumField.post_process <pcapkit.corekit.fields.numbers.EnumField.post_process>` already makes for the same reason. Args: key: Name or value to look up. default: 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 :issue:`864`. :data:`~pcapkit.corekit.sentinels.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. 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 :meth:`_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 :exc:`ValueError`, so an ``except ValueError`` caller is unaffected. Quiet. EnumKeyError: If a name does not resolve and there is no usable default. A :exc:`KeyError`, so an ``except KeyError`` caller is unaffected. Quiet. .. versionchanged:: 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 :meth:`EnumRegistry.register` still reports the same range rejection loudly (:issue:`1845`). """ if isinstance(key, str): try: return cls._member_map_[key] except KeyError: if key in cls._value2member_map_: return cls._value2member_map_[key] if default is NO_DEFAULT or default not in cls._value2member_map_: raise EnumKeyError(f'{key!r} is not a valid {cls.__name__}', quiet=True) from None return cls._value2member_map_[default] try: # NOTE: GitHub issue #1845. Every ValueError raised in this block is # this method's own lookup miss -- answered with ``default`` below, # or raised as the miss -- so a loud one is quiet here, whoever # constructed it. with CaughtByCaller(ValueError): cls._validate_value(key) return cls(key) # type: ignore[call-arg] except ValueError as error: if default is NO_DEFAULT or default not in cls._value2member_map_: if isinstance(error, BaseError): raise raise EnumValueError(str(error), quiet=True) from error return cls._value2member_map_[default]
[docs] @classmethod def get_all(cls, key: 'Any') -> 'tuple[Self, ...]': """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 :meth:`~EnumRegistry.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 :issue:`842`), and it is where a registry with genuinely several matches puts them: :class:`~pcapkit.const.reg.apptype.apptype.AppType` overrides it to return every service IANA assigns to a port. Args: key: Name or value to look up. Returns: The canonical member, followed by any further distinct member carrying the same value. Raises: EnumValueError: As :meth:`get` with no default, for a value. EnumKeyError: As :meth:`get` with no default, for a name. """ canonical = cls.get(key) return (canonical, *( member for member in cls._member_map_.values() if member is not canonical and member.value == canonical.value # type: ignore[attr-defined] ))
[docs] class EnumRegistry(EnumLookup): """Registry protocol shared by every constant enumeration under :mod:`pcapkit.const`. :class:`EnumLookup` above carries the read half -- :meth:`~EnumLookup.get`, :meth:`~EnumLookup.get_all` and :meth:`~EnumLookup._validate_value`, all inherited here unchanged. What this tier adds is the half that makes a registry *open*: :meth:`register`, :meth:`register_alias`, :meth:`register_aliases`, :meth:`_extend` and :meth:`_unregistered_member`. An enumeration inherits from *here* when it may grow at runtime, and from :class:`EnumLookup` directly when it may not. The owner's ruling on GitHub issue :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 :class:`EnumLookup` as a parent does not disturb that: both tiers are plain classes, so ``_member_type_`` still resolves past them to the enumeration base. """
[docs] @classmethod def register(cls, value: 'Any', name: 'str') -> 'Self': """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 :issue:`842`). Contrast :meth:`~EnumLookup.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 :meth:`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 :meth:`register_alias` tests it that way: a declared-but-unassigned value resolves through ``_missing_`` to an :meth:`_unregistered_member` absent from that table, so a successful call proves nothing about whether a member already exists. Routes through :meth:`~EnumLookup._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. :meth:`~EnumLookup.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 :class:`~pcapkit.utilities.exceptions.CaughtByCaller` block declaring that nothing is caught -- loud even if ``register`` is reached from inside a ``get`` (:issue:`1845`). Args: value: Value of the new member. name: Name of the new member. Required rather than derived, which is the whole point -- a generated name is a guess. Returns: The newly registered member. Raises: ValueError: If ``value`` already has a member -- use :meth:`register_alias` to add a further name for it instead. EnumValueError: If a subclass's :meth:`~EnumLookup._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 :exc:`TypeError`; it is translated so that the ways one call can fail are one exception type. """ if value in cls._value2member_map_: existing = cls._value2member_map_[value] raise ValueError(f'{value!r} is already registered on {cls.__name__} as ' f'{existing.name!r}; use {cls.__name__}.register_alias() ' f'to add a further name for it') # NOTE: GitHub issue #1845. Declares that nothing is caught, so the # rejection stays loud even inside a ``get`` block that would quiet it. with CaughtByCaller(): cls._validate_value(value) return cls._extend(value, name)
[docs] @classmethod def _extend(cls, value: 'Any', name: 'str') -> 'Self': """The raw ``aenum.extend_enum`` call, shared by :meth:`register` and :meth:`register_alias`. Neither public method calls the other: :meth:`register` refuses an already-registered ``value`` before it would ever reach here, and :meth:`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 :meth:`register_alias` while :meth:`register` still rejects it. Args: value: Value of the member, new or existing. name: Name to add. Returns: The member now reachable under ``name``, new or existing. Raises: ValueError: If ``name`` is already taken. |aenum|_ reports that as :exc:`TypeError`; it is translated so that the ways one call can fail are one exception type. """ try: return extend_enum(cls, name, value) except TypeError as error: raise ValueError(str(error)) from error
[docs] @classmethod def register_alias(cls, value: 'Any', name: 'str') -> 'Self': """Add ``name`` as a further name for the member already at ``value``. Per GitHub issue :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 :meth:`register`. :issue:`842` settled on an alias always attaching to an existing member, leaving open whether :class:`~pcapkit.const.reg.apptype.apptype.AppType` or a concrete enumeration might need to alias a value no member carries. :class:`~pcapkit.const.reg.apptype.apptype.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 :class:`~pcapkit.const.reg.apptype.apptype.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 :issue:`775` removes. Membership is tested against ``_value2member_map_`` rather than by calling ``cls(value)``: a declared-but-unassigned value resolves through ``_missing_`` to an :meth:`_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 :meth:`_extend` directly rather than :meth:`register`, which would refuse this call outright -- :meth:`register` and :meth:`register_alias` test ``value``'s membership for opposite outcomes, so neither can be the other's implementation. :class:`~pcapkit.const.reg.apptype.apptype.AppType`'s override differs: it mints a real member through ``aenum.extend_enum``, so its iteration grows too. Args: value: Value of the existing member to alias. name: Alias to add for it. Returns: The existing member, now reachable under ``name`` as well. Raises: ValueError: If no member carries ``value``, or if ``name`` is already taken. """ if value not in cls._value2member_map_: raise ValueError(f'{value!r} is not a registered {cls.__name__}; ' f'use {cls.__name__}.register() to mint one') return cls._extend(value, name)
[docs] @classmethod def register_aliases(cls, value: 'Any', *names: 'str') -> 'tuple[Self, ...]': """Add several aliases for the member at ``value``, left to right. Args: value: Value of the existing member to alias. *names: Aliases to add for it. Returns: One entry per name in ``names``, each the aliased member. Raises: ValueError: As :meth:`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. """ return tuple(cls.register_alias(value, name) for name in names)
[docs] @classmethod def _unregistered_member(cls, value: 'Any', name: 'str') -> 'Self': """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 :meth:`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. Args: value: The member's value. name: The member's name. Returns: The unregistered member. """ obj = cls._member_type_.__new__(cls, value) obj._name_ = name # pylint: disable=protected-access obj._value_ = value # pylint: disable=protected-access return obj