# -*- 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