Source code for pcapkit.corekit.enum

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

.. module:: pcapkit.corekit.enum

:mod:`pcapkit.corekit.enum` contains the two 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 enumeration under
  :mod:`pcapkit.const` inherits from here.

That split is the owner's ruling on GitHub issue #877, verbatim: *"My initial
thought was to make them immutable - unless RFC/IANA says otherwise. Therefore
they may subclass a bare base enum from pcapkit.corekit.enum - where
EnumRegistry subclasses it for using in the other mutable ones."*

Which methods land on which tier was settled in the same thread. The owner's own
second thought is what drew the line: *"if it carries ``register``, then why not
``register_alias``. We might be creating a bad ruling."* 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 all five mutating
methods stay put, and what the base carries instead is the owner's other requirement,
verbatim: *"there must be some sort of range validation logic for the inherited
classes to hook in"* -- which is :meth:`EnumLookup._validate_value`. Legality is
every enumeration's concern; mutation is only the open registries'.

Supporting measurement, taken on this tree at the time of the split: **0** of the
26 non-registry enumerations define ``register``, ``register_alias``,
``register_aliases`` or ``_unregistered_member``, and there are **0**
:class:`EnumRegistry` subclasses outside :mod:`pcapkit.const`. The mutating half
therefore had no users to serve among the classes being re-parented.

.. note::

   Re-parenting every non-registry enumeration onto :class:`EnumLookup` was
   **phase 2** of GitHub issue #877, and it is now **complete**: introducing
   the base above was deliberately behaviour-preserving on its own, so that it
   could land while other work was still in flight on the files the re-parent
   touches, and the phase itself landed in two pull requests for exactly that
   reason -- #921 for the 17 enumerations that were free to move at once, and
   `#930 <https://github.com/JarryShaw/PyPCAPKit/issues/930>`__ for the
   remaining seven once the files holding them freed up.

The registry tier's own shape is the earlier ruling on GitHub issue #842,
verbatim: *"to finalise the abstraction idea, get/get_all/register/register_alias
should always exist on the const enums - so they're to be moved to the base
class. And AppType's sub-base class will do its necessary overrides and
dispatching logic; AppType subclasses will have their necessary overrides again
pertaining their different contracts."*

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

1. :class:`EnumRegistry` -- the four methods, in the form that suits a registry
   mapping one key to one member. Every generated enumeration under
   :mod:`pcapkit.const` inherits them from here.
2. ``AppType``'s sub-base -- overrides all four to route through its
   ``_dispatch``, because a port lookup needs a transport protocol to be
   answerable at all. Landed as of GitHub issue #860: not in this module, but
   in :class:`pcapkit.const.reg.apptype.apptype.AppType` itself, which now
   mixes in :class:`EnumRegistry` directly and overrides ``get``, ``get_all``,
   ``register`` and ``register_alias`` with that dispatch, plus
   ``_unregistered_member`` for its own three extra attributes (``svc``,
   ``port``, ``proto``) that the generic one below does not know to set.
3. The ``AppType`` transport subclasses -- ``TCP``, ``UDP``, ``SCTP``, ``DCCP``
   -- turned out to 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 four, so tier 2's methods
   already answer correctly on each of them without a further layer.

Before this, the four methods lived as generated *text*: written out longhand in
:data:`pcapkit.vendor.default.LINE` and copied verbatim into each of the eleven
crawlers that replace that template wholesale, none of which carried
``register``, ``register_alias`` or ``get_all`` at all. Adding one method meant
editing every bespoke template by hand, which is the cost #775 asks to remove.

The contracts are the maintainer's, verbatim: *"get is a shortcut for ``[]``
operation and returns the canonical enum. get_all returns all matching enums.
register mints new enum to the class at runtime with specified names - so we
don't have to guess blindly. register_alias(es) adds additional alias(es) to a
given enum's mapping."*

"""
from typing import TYPE_CHECKING

from aenum import extend_enum

from pcapkit.corekit.sentinels import NO_DEFAULT, NoDefaultType  # pylint: disable=unused-import
from pcapkit.utilities.exceptions import BaseError, EnumKeyError, EnumValueError

if TYPE_CHECKING:
    from typing import Any

    from typing_extensions import Self

__all__ = ['NO_DEFAULT', '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 :class:`~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 :mod:`aenum` still resolves the member data type from the enumeration base: ``int`` for :class:`~aenum.IntEnum` and :class:`~aenum.IntFlag`, ``str`` for :class:`~aenum.StrEnum`. That is what lets one base serve all three, 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 :class:`~aenum.Enum` in order to "be an enum", it would have become the member type itself and broken all three shapes at once. The methods deliberately touch only ``_member_map_``, ``_member_names_`` and ``_value2member_map_``, which both :mod:`enum` and :mod:`aenum` maintain, so nothing on this tier depends on :mod:`aenum` internals at all -- the one :func:`~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 :class:`~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 -- five 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. The owner's requirement on GitHub issue #877, verbatim: *"there must be some sort of range validation logic for the inherited classes to hook in."* 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 -- the shape ``_missing_`` spells by hand across the generated registries today:: @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 owner's ruling on GitHub issue #877 rules out -- *"enum should honour and keep their original writings as in the registrars."* 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. An override raising something outside that hierarchy would instead propagate past ``default``, 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 and the single log record it already emitted are what the caller sees. Called from exactly two places, 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. :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 #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 one registry (``CGAType``) -- the ruling's final round converted the other two it originally held out, ``EtherType`` and ``Socket``, so they no longer mint on any path either. Registering a member any other way is :meth:`register`'s job and nobody else's, which is the ruling #775 exists to carry out: *"so that we dont create registered enums out of unrecognised/unregistered values, unless user/caller explicitly created them"*. A value inside a registry's declared-but-unassigned range still resolves, through that registry's own ``_missing_`` and :meth:`_unregistered_member`, to a member that is deliberately absent from the lookup tables -- true outside the one registry named above, where such a value instead lands in *both* tables, exactly as :meth:`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, matching what already happened for a name that resolves today. 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 previously just raised. Defensive rather than observed: of the 127 classes that reach this method -- 125 until GitHub issue #880's own PR added ``pcapkit/const/ngap/procedure_code.py`` and ``pcapkit/const/ngap/protocol_ie.py``, remeasured while auditing GitHub issue #903 -- the ``str``-valued ones (: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 four 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` -- completing that set as of GitHub issue #860's own PR 2 -- and, newest of them, :class:`~pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`, which GitHub issue #877's own thread reclassified from a hand-written helper to a generated registry once RFC 9850 §4.2 turned its member list into a live IANA registry) no longer mint on any path, so no live witness exists in this tree today. The one registry that still mints directly via :func:`~aenum.extend_enum`, :class:`~pcapkit.const.mh.cga_type.CGAType`, 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. GitHub issue #775's final round converted the other two that used to share this footnote, :class:`~pcapkit.const.ipx.socket.Socket` and :class:`~pcapkit.const.reg.ethertype.EtherType`, so ``CGAType`` is now the only one left. 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_``. Since #864, that is no longer merely a claim about the value side alone: ``default`` resolves through the same kind of ``_value2member_map_`` lookup rather than ``cls(default)``, 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:`_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. Both failure paths raise from :mod:`pcapkit.utilities.exceptions` rather than a builtin, per the owner's ruling on GitHub issue #923: *"Either ``ValueError`` or ``KeyError``, that's depending on how stdlib's ``Enum`` would raise on these circumstances. And we should raise one from ``pcapkit.utilities.exceptions`` rather builtin exceptions."* The *shape* is unchanged by that ruling and deliberately so -- a name miss stays :exc:`KeyError`-derived and a value miss :exc:`ValueError`-derived, matching ``E['nosuch']`` and ``E(999)`` on a stdlib :class:`~enum.Enum`, and matching the 119 of this tree's 127 concrete subclasses that already answered a name miss that way. Only the provenance changed, so every ``except KeyError`` and ``except ValueError`` around a call to this method keeps catching. Two details of that conversion are worth stating, since neither is visible from the exception type alone: * **The name miss is raised quietly** -- :exc:`~pcapkit.utilities.exceptions.EnumKeyError` with ``quiet=True``, so nothing is logged and :data:`sys.tracebacklimit` is left alone. That is not a cosmetic choice: this method's name miss is in-library control flow at six call sites, and at :meth:`~pcapkit.const.http.method.Method.get` it is part of a *successful* call -- that override catches it in order to mint. A loud error there would put a :data:`logging.CRITICAL` record on every such call and set :data:`sys.tracebacklimit` to ``0`` process-wide, which is exactly the GitHub issue #362 defect :class:`~pcapkit.utilities.exceptions.BaseError` documents ``quiet`` for. The value miss takes no such fallback anywhere in this tree, so it 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 the error is logged once instead of twice. Only :mod:`aenum`'s and :mod:`enum`'s own "no member carries this value" is converted. 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 #864. :data:`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. EnumKeyError: If a name does not resolve and there is no usable default. A :exc:`KeyError`, so an ``except KeyError`` caller is unaffected. """ 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: 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)) 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:`register_alias` is a second *name* for the canonical member rather than a second member. The method still exists here, per the ruling that all four *"should always exist on the const enums"*, and it is where a registry with genuinely several matches puts them: ``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 #877 is what draws that line, verbatim: *"My initial thought was to make them immutable - unless RFC/IANA says otherwise."* Mixed in ahead of the enum base exactly as before -- ``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: *"register mints new enum to the class at runtime with specified names - so we don't have to guess blindly"*. Contrast :meth:`get` and ``_missing_``, which resolve without naming anything. Refuses a ``value`` that already has a member. Without this guard, :func:`~aenum.extend_enum` does not mint anything for an already-taken value -- :mod:`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. 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. :mod:`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') cls._validate_value(value) return cls._extend(value, name)
[docs] @classmethod def _extend(cls, value: 'Any', name: 'str') -> 'Self': """The raw :func:`~aenum.extend_enum` call, shared by :meth:`register` and :meth:`register_alias`. Neither public method calls the other: :meth:`register` now 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. :mod:`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 the ruling, an alias *"adds additional alias(es) to a given enum's mapping"* -- so it needs an enum to be given, and this refuses a value no member carries rather than falling through to :meth:`register`. Asked whether that should hold generally, the maintainer's answer was *"actually i think it should always be for an existing member"*, and on what an alias means away from ``AppType``: *"For non-AppType registries, 'Alias' is custom/caller-opt-in names, which are not recorded in 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 :meth:`_unregistered_member` that is deliberately absent from that table, so a successful call proves nothing about whether a member exists. 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 now 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 any more. 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 -- :func:`~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 no longer grows the registry -- contrast :meth:`register`, the explicit path that still does. The member is constructed through ``cls._member_type_``, which :mod:`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