From cb7eb0d9438782782a0c2053c4eccb4da0d71055 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 27 Sep 2026 10:19:15 -0400 Subject: [PATCH] refactor(corekit): move the const registry protocol onto a base class (#775) Tier 2 of #775, the abstraction half. Per the ruling on #842 -- "get/get_all/register/register_alias should always exist on the const enums - so they're to be moved to the base class" -- the four methods now live once, on `pcapkit.corekit.enums.EnumRegistry`, instead of being written out as generated text in `pcapkit/vendor/default.py`'s template and hand-copied into each of the eleven crawlers that override it. - Add `EnumRegistry`, a plain mix-in carrying `get`, `get_all`, `register`, `register_alias`, `register_aliases` and `_unregistered_member`, with each method's contract as ruled. - `register` now refuses a value that already has a member instead of silently aliasing it via `aenum.extend_enum` under the caller's name -- the inverse of the check `register_alias` already ran on `value`. Both now route through a new shared `_extend`, so `register_alias` keeps the mint-or-alias behaviour it depends on. - Convert six bespoke templates onto the base -- the four `mh/*_flag`, `ipv6/extension_header`, and `tcp/flags` -- and regenerate. `tcp/flags` was scoped out on the claim that it "attaches extra attributes in `__new__`"; measured, it defines none, the same shape as the four `mh/*_flag` templates, so it joins them instead of the five that do. - `get`'s string miss now honours `default` on those six, where their own copy ended in a bare `return NAME[key]`. - Update the template-render regexes in `test_const_enum_lookup.py` and `test_const_enum_builtin_parity.py` for the new class statement. No behaviour change for the other 115 registries, nor for `tcp/flags`'s own range-checking `_missing_`, which still ends in `super()._missing_(value)` unchanged. `tests/const` and `tests/vendor` pass. --- pcapkit/const/ipv6/extension_header.py | 27 +- pcapkit/const/mh/binding_ack_flag.py | 25 +- pcapkit/const/mh/binding_update_flag.py | 25 +- pcapkit/const/mh/handover_ack_flag.py | 25 +- pcapkit/const/mh/handover_initiate_flag.py | 25 +- pcapkit/const/tcp/flags.py | 28 +- pcapkit/corekit/__init__.py | 3 + pcapkit/corekit/enums.py | 349 +++++++++ pcapkit/vendor/ipv6/extension_header.py | 27 +- pcapkit/vendor/mh/binding_ack_flag.py | 25 +- pcapkit/vendor/mh/binding_update_flag.py | 25 +- pcapkit/vendor/mh/handover_ack_flag.py | 25 +- pcapkit/vendor/mh/handover_initiate_flag.py | 25 +- pcapkit/vendor/tcp/flags.py | 28 +- tests/const/test_const_enum_builtin_parity.py | 11 +- tests/const/test_const_enum_lookup.py | 7 +- tests/const/test_const_registry_protocol.py | 678 ++++++++++++++++++ 17 files changed, 1082 insertions(+), 276 deletions(-) create mode 100644 pcapkit/corekit/enums.py create mode 100644 tests/const/test_const_registry_protocol.py diff --git a/pcapkit/const/ipv6/extension_header.py b/pcapkit/const/ipv6/extension_header.py index c1bf2786c0..a6b9d9cd2b 100644 --- a/pcapkit/const/ipv6/extension_header.py +++ b/pcapkit/const/ipv6/extension_header.py @@ -10,12 +10,14 @@ """ -from aenum import IntEnum, extend_enum +from aenum import IntEnum + +from pcapkit.corekit.enums import EnumRegistry __all__ = ['ExtensionHeader'] -class ExtensionHeader(IntEnum): +class ExtensionHeader(EnumRegistry, IntEnum): """[ExtensionHeader] IPv6 Extension Header Types""" #: HOPOPT, IPv6 Hop-by-Hop Option [:rfc:`8200`] @@ -53,24 +55,3 @@ class ExtensionHeader(IntEnum): #: Use for experimentation and testing [:rfc:`3692`] Use_for_experimentation_and_testing_254 = 254 - - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> 'ExtensionHeader': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return ExtensionHeader(key) - except ValueError: - if default == -1: - raise - return ExtensionHeader(default) - return ExtensionHeader[key] # type: ignore[misc] diff --git a/pcapkit/const/mh/binding_ack_flag.py b/pcapkit/const/mh/binding_ack_flag.py index 2258a31e02..84582b0263 100644 --- a/pcapkit/const/mh/binding_ack_flag.py +++ b/pcapkit/const/mh/binding_ack_flag.py @@ -12,10 +12,12 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['BindingACKFlag'] -class BindingACKFlag(IntFlag): +class BindingACKFlag(EnumRegistry, IntFlag): """[BindingACKFlag] Binding Acknowledgment Flags""" #: K [:rfc:`6275`] @@ -39,27 +41,6 @@ class BindingACKFlag(IntFlag): #: D [:rfc:`8885`] D = 0x02 - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> 'BindingACKFlag': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return BindingACKFlag(key) - except ValueError: - if default == -1: - raise - return BindingACKFlag(default) - return BindingACKFlag[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> 'BindingACKFlag': """Lookup function used when value is not found. diff --git a/pcapkit/const/mh/binding_update_flag.py b/pcapkit/const/mh/binding_update_flag.py index 6149634bb2..5b835fb866 100644 --- a/pcapkit/const/mh/binding_update_flag.py +++ b/pcapkit/const/mh/binding_update_flag.py @@ -12,10 +12,12 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['BindingUpdateFlag'] -class BindingUpdateFlag(IntFlag): +class BindingUpdateFlag(EnumRegistry, IntFlag): """[BindingUpdateFlag] Binding Update Flags""" #: A [:rfc:`6275`] @@ -54,27 +56,6 @@ class BindingUpdateFlag(IntFlag): #: D [:rfc:`8885`] D = 0x0010 - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> 'BindingUpdateFlag': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return BindingUpdateFlag(key) - except ValueError: - if default == -1: - raise - return BindingUpdateFlag(default) - return BindingUpdateFlag[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> 'BindingUpdateFlag': """Lookup function used when value is not found. diff --git a/pcapkit/const/mh/handover_ack_flag.py b/pcapkit/const/mh/handover_ack_flag.py index 61a3a3dd27..8c23f13ea5 100644 --- a/pcapkit/const/mh/handover_ack_flag.py +++ b/pcapkit/const/mh/handover_ack_flag.py @@ -12,10 +12,12 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['HandoverACKFlag'] -class HandoverACKFlag(IntFlag): +class HandoverACKFlag(EnumRegistry, IntFlag): """[HandoverACKFlag] Handover Acknowledge Flags""" #: Buffer flag [:rfc:`5949`] @@ -27,27 +29,6 @@ class HandoverACKFlag(IntFlag): #: Forwarding flag [:rfc:`5949`] F = 0x20 - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> 'HandoverACKFlag': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return HandoverACKFlag(key) - except ValueError: - if default == -1: - raise - return HandoverACKFlag(default) - return HandoverACKFlag[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> 'HandoverACKFlag': """Lookup function used when value is not found. diff --git a/pcapkit/const/mh/handover_initiate_flag.py b/pcapkit/const/mh/handover_initiate_flag.py index 2b3f0458ae..aa6d63c16d 100644 --- a/pcapkit/const/mh/handover_initiate_flag.py +++ b/pcapkit/const/mh/handover_initiate_flag.py @@ -12,10 +12,12 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['HandoverInitiateFlag'] -class HandoverInitiateFlag(IntFlag): +class HandoverInitiateFlag(EnumRegistry, IntFlag): """[HandoverInitiateFlag] Handover Initiate Flags""" #: Assigned Address Configuration flag [:rfc:`5568`] @@ -30,27 +32,6 @@ class HandoverInitiateFlag(IntFlag): #: Forwarding flag [:rfc:`5949`] F = 0x10 - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> 'HandoverInitiateFlag': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return HandoverInitiateFlag(key) - except ValueError: - if default == -1: - raise - return HandoverInitiateFlag(default) - return HandoverInitiateFlag[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> 'HandoverInitiateFlag': """Lookup function used when value is not found. diff --git a/pcapkit/const/tcp/flags.py b/pcapkit/const/tcp/flags.py index 9100d69873..7419ceea97 100644 --- a/pcapkit/const/tcp/flags.py +++ b/pcapkit/const/tcp/flags.py @@ -10,16 +10,13 @@ """ -from typing import TYPE_CHECKING - from aenum import IntFlag -if TYPE_CHECKING: - from typing import Optional +from pcapkit.corekit.enums import EnumRegistry __all__ = ['Flags'] -class Flags(IntFlag): +class Flags(EnumRegistry, IntFlag): """[Flags] TCP Header Flags""" #: Reserved for future use [:rfc:`9293`] @@ -58,27 +55,6 @@ class Flags(IntFlag): #: No more data from sender (FIN) [:rfc:`9293`] FIN = 1 << 15 - @staticmethod - def get(key: 'int | str', default: 'Optional[int]' = -1) -> 'Flags': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return Flags(key) - except ValueError: - if default == -1: - raise - return Flags(default) - return Flags[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> 'Flags': """Lookup function used when value is not found. diff --git a/pcapkit/corekit/__init__.py b/pcapkit/corekit/__init__.py index fe4840c4c8..e8c4bb177f 100644 --- a/pcapkit/corekit/__init__.py +++ b/pcapkit/corekit/__init__.py @@ -17,6 +17,7 @@ class :class:`~pcapkit.corekit.infoclass.Info`, """ from pcapkit.corekit.io import SeekableReader +from pcapkit.corekit.enums import EnumRegistry from pcapkit.corekit.fields import * from pcapkit.corekit.infoclass import Info, info_final from pcapkit.corekit.module import ModuleDescriptor @@ -25,6 +26,8 @@ class :class:`~pcapkit.corekit.infoclass.Info`, from pcapkit.corekit.version import VersionInfo __all__ = [ + 'EnumRegistry', + 'Info', 'info_final', 'ProtoChain', diff --git a/pcapkit/corekit/enums.py b/pcapkit/corekit/enums.py new file mode 100644 index 0000000000..ceb500e92b --- /dev/null +++ b/pcapkit/corekit/enums.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +"""Constant Enumeration Base +============================== + +.. module:: pcapkit.corekit.enums + +:mod:`pcapkit.corekit.enums` contains :class:`~pcapkit.corekit.enums.EnumRegistry` +only, the base class every constant enumeration under :mod:`pcapkit.const` is to +inherit the registry protocol from. + +The maintainer's 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. Not in this module, and not yet written: today's + :class:`pcapkit.const.reg.apptype.apptype.AppType` carries that logic + directly and stays as it is until tier two lands. +3. The ``AppType`` transport subclasses -- ``TCP``, ``UDP``, ``SCTP``, ``DCCP`` + -- override again for their own contracts. + +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 + +if TYPE_CHECKING: + from typing import Any + + from typing_extensions import Self + +__all__ = ['EnumRegistry'] + +#: The ``default`` argument value that means *no default*, i.e. let an +#: unresolvable key propagate its lookup error rather than falling back. +#: +#: ``-1`` rather than a sentinel object because that is what the 121 generated +#: registries already document and what their callers already pass, so the +#: migration onto this base class is not also a signature change. It is a safe +#: sentinel for both member types in play: no registry generated from an +#: upstream assignment carries a negative code, and ``-1`` is not a +#: :class:`str`, so a :class:`~aenum.StrEnum` registry can never mistake it for +#: one of its own values either. +NO_DEFAULT = -1 + + +class EnumRegistry: + """Registry protocol shared by every constant enumeration under + :mod:`pcapkit.const`. + + 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)`` -- 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. + + The methods deliberately touch only ``_member_map_``, ``_member_names_`` and + ``_value2member_map_``, which both :mod:`enum` and :mod:`aenum` maintain, so + nothing here depends on :mod:`aenum` internals beyond + :func:`~aenum.extend_enum` itself. + + """ + + 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' + + @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. Registering a member 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. + + Args: + key: Name or value to look up. + default: Value to fall back to when ``key`` does not resolve. + :data:`NO_DEFAULT` stands for *no default*, in which case the + lookup error propagates instead. + + Returns: + The canonical member for ``key``, or for ``default``. + + Raises: + ValueError: If a value does not resolve and there is no usable + default. + KeyError: If a name does not resolve and there is no usable + default. + + """ + if isinstance(key, str): + try: + return cls._member_map_[key] + except KeyError: + if default == NO_DEFAULT: + raise + return cls(default) # type: ignore[call-arg] + try: + return cls(key) # type: ignore[call-arg] + except ValueError: + if default == NO_DEFAULT: + raise + return cls(default) # type: ignore[call-arg] + + @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: + ValueError: As :meth:`get` with no default, for a value. + KeyError: 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] + )) + + @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. + + 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. + 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') + return cls._extend(value, name) + + @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 + + @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) + + @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) + + @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 diff --git a/pcapkit/vendor/ipv6/extension_header.py b/pcapkit/vendor/ipv6/extension_header.py index 37a9f37b03..b93cd9e0cf 100644 --- a/pcapkit/vendor/ipv6/extension_header.py +++ b/pcapkit/vendor/ipv6/extension_header.py @@ -36,36 +36,17 @@ """ -from aenum import IntEnum, extend_enum +from aenum import IntEnum + +from pcapkit.corekit.enums import EnumRegistry __all__ = ['{NAME}'] -class {NAME}(IntEnum): +class {NAME}(EnumRegistry, IntEnum): """[{NAME}] {DOCS}""" {ENUM} - - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return {NAME}(key) - except ValueError: - if default == -1: - raise - return {NAME}(default) - return {NAME}[key] # type: ignore[misc] ''' # type: Callable[[str, str, str, str], str] diff --git a/pcapkit/vendor/mh/binding_ack_flag.py b/pcapkit/vendor/mh/binding_ack_flag.py index f81eff18ce..3010b38e22 100644 --- a/pcapkit/vendor/mh/binding_ack_flag.py +++ b/pcapkit/vendor/mh/binding_ack_flag.py @@ -37,35 +37,16 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['{NAME}'] -class {NAME}(IntFlag): +class {NAME}(EnumRegistry, IntFlag): """[{NAME}] {DOCS}""" {ENUM} - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return {NAME}(key) - except ValueError: - if default == -1: - raise - return {NAME}(default) - return {NAME}[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> '{NAME}': """Lookup function used when value is not found. diff --git a/pcapkit/vendor/mh/binding_update_flag.py b/pcapkit/vendor/mh/binding_update_flag.py index 57648cff3a..790a222305 100644 --- a/pcapkit/vendor/mh/binding_update_flag.py +++ b/pcapkit/vendor/mh/binding_update_flag.py @@ -36,35 +36,16 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['{NAME}'] -class {NAME}(IntFlag): +class {NAME}(EnumRegistry, IntFlag): """[{NAME}] {DOCS}""" {ENUM} - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return {NAME}(key) - except ValueError: - if default == -1: - raise - return {NAME}(default) - return {NAME}[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> '{NAME}': """Lookup function used when value is not found. diff --git a/pcapkit/vendor/mh/handover_ack_flag.py b/pcapkit/vendor/mh/handover_ack_flag.py index 3069468aa7..7e84c00832 100644 --- a/pcapkit/vendor/mh/handover_ack_flag.py +++ b/pcapkit/vendor/mh/handover_ack_flag.py @@ -37,35 +37,16 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['{NAME}'] -class {NAME}(IntFlag): +class {NAME}(EnumRegistry, IntFlag): """[{NAME}] {DOCS}""" {ENUM} - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return {NAME}(key) - except ValueError: - if default == -1: - raise - return {NAME}(default) - return {NAME}[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> '{NAME}': """Lookup function used when value is not found. diff --git a/pcapkit/vendor/mh/handover_initiate_flag.py b/pcapkit/vendor/mh/handover_initiate_flag.py index 7a37484a05..b7fecc6d6d 100644 --- a/pcapkit/vendor/mh/handover_initiate_flag.py +++ b/pcapkit/vendor/mh/handover_initiate_flag.py @@ -37,35 +37,16 @@ from aenum import IntFlag +from pcapkit.corekit.enums import EnumRegistry + __all__ = ['{NAME}'] -class {NAME}(IntFlag): +class {NAME}(EnumRegistry, IntFlag): """[{NAME}] {DOCS}""" {ENUM} - @staticmethod - def get(key: 'int | str', default: 'int' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return {NAME}(key) - except ValueError: - if default == -1: - raise - return {NAME}(default) - return {NAME}[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> '{NAME}': """Lookup function used when value is not found. diff --git a/pcapkit/vendor/tcp/flags.py b/pcapkit/vendor/tcp/flags.py index 42072480ff..5228207711 100644 --- a/pcapkit/vendor/tcp/flags.py +++ b/pcapkit/vendor/tcp/flags.py @@ -47,41 +47,17 @@ """ -from typing import TYPE_CHECKING - from aenum import IntFlag -if TYPE_CHECKING: - from typing import Optional +from pcapkit.corekit.enums import EnumRegistry __all__ = ['{NAME}'] -class {NAME}(IntFlag): +class {NAME}(EnumRegistry, IntFlag): """[{NAME}] {DOCS}""" {ENUM} - @staticmethod - def get(key: 'int | str', default: 'Optional[int]' = -1) -> '{NAME}': - """Backport support for original codes. - - Args: - key: Key to get enum item. - default: Default value if not found. The placeholder ``-1`` stands - for *no default*, in which case an unresolvable key propagates - the lookup error instead of falling back. - - :meta private: - """ - if isinstance(key, int): - try: - return Flags(key) - except ValueError: - if default == -1: - raise - return Flags(default) - return {NAME}[key] # type: ignore[misc] - @classmethod def _missing_(cls, value: 'int') -> '{NAME}': """Lookup function used when value is not found. diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 74edb145d4..acf0dd03d8 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -1045,6 +1045,15 @@ def test_the_tcp_flags_template_renders_the_committed_module(self) -> None: :mod:`tests.const.test_const_enum_lookup`, which does this for the four Mobility Header templates. Needs no network: the crawl supplies only the enumeration block, which is read back out of the committed module. + + NOTE: the class statement names ``EnumRegistry`` and the first method + after the enumeration block is a ``classmethod``, because GitHub issue + #775's tier 2 found ``tcp/flags``' own exclusion rationale did not hold + -- see ``tests.const.test_const_registry_protocol`` -- and converted it + onto :class:`~pcapkit.corekit.enums.EnumRegistry` alongside the four + ``mh/*_flag`` templates. The generated ``@staticmethod get`` that used + to close the block is gone, leaving ``_missing_`` next, the same + rewrite ``test_const_enum_lookup.py`` made for those four. """ import pathlib @@ -1059,7 +1068,7 @@ def test_the_tcp_flags_template_renders_the_committed_module(self) -> None: ).read_text(encoding='utf-8') block = re.compile( - r'class \w+\(IntFlag\):\n """.*?"""\n\n (.*?)\n\n @staticmethod', re.S) + r'class \w+\(EnumRegistry, IntFlag\):\n """.*?"""\n\n (.*?)\n\n @classmethod', re.S) enum_block = block.search(committed) self.assertIsNotNone(enum_block, 'no enumeration block in pcapkit.const.tcp.flags') diff --git a/tests/const/test_const_enum_lookup.py b/tests/const/test_const_enum_lookup.py index 7c4c0ce09b..d533f751f0 100644 --- a/tests/const/test_const_enum_lookup.py +++ b/tests/const/test_const_enum_lookup.py @@ -400,8 +400,13 @@ def test_the_vendor_templates_still_emit_the_fix(self) -> None: """ import pathlib + # NOTE: the class statement names EnumRegistry and the first method after + # the enumeration block is a classmethod, because #775's tier 2 moved + # get/get_all/register/register_alias off these templates and onto + # pcapkit.corekit.enums.EnumRegistry -- the generated `@staticmethod get` + # that used to close the block is gone, leaving `_missing_` next. block = re.compile( - r'class \w+\(IntFlag\):\n """.*?"""\n\n (.*?)\n\n @staticmethod', + r'class \w+\(EnumRegistry, IntFlag\):\n """.*?"""\n\n (.*?)\n\n @classmethod', re.S) for stem, name, _, _ in MH_FLAG_ENUMS: diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py new file mode 100644 index 0000000000..71711112e5 --- /dev/null +++ b/tests/const/test_const_registry_protocol.py @@ -0,0 +1,678 @@ +# -*- coding: utf-8 -*- +"""Tests for :class:`pcapkit.corekit.enums.EnumRegistry`, tier 2 of issue #775. + +Tier 1 (#838) removed the mint from the two sites in +:data:`pcapkit.vendor.default.LINE` that the 105 default-template registries +inherit. It could not reach the eleven crawlers that replace that template with +their own, because each of those carries a hand-copied ``get()`` -- and none of +them carries ``register``, ``register_alias`` or ``get_all`` at all. + +The maintainer's ruling on #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."* + +:class:`~pcapkit.corekit.enums.EnumRegistry` is tier one of that hierarchy. This +module pins both halves of the claim: that the generated registries in this batch +really do inherit the protocol rather than carry a copy of it, and that each of +the four methods honours the contract the maintainer wrote for it. + +""" +from __future__ import annotations + +import importlib +import pathlib +import unittest +from typing import TYPE_CHECKING + +from aenum import IntEnum, IntFlag, StrEnum + +from pcapkit.corekit.enums import EnumRegistry +from tests._support import ISOLATED_PREFIXES, purge_modules, restore_modules, snapshot_modules + +if TYPE_CHECKING: + from typing import Any + +#: Repository root, for reading generated sources as text. +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] + +#: The batch converted onto :class:`~pcapkit.corekit.enums.EnumRegistry`: the +#: six bespoke-template registries whose members carry no extra attributes, so +#: the shared ``_unregistered_member`` -- which sets only ``_name_`` and +#: ``_value_`` -- builds a complete member for them. ``tcp/flags`` joined the +#: original five once measurement showed its exclusion rationale did not hold: +#: the PR description had grouped it with the five templates below on the +#: strength of "each attach extra attributes in ``__new__``", but +#: ``grep -c 'def __new__'`` reports ``0`` for both ``pcapkit/vendor/tcp/flags.py`` +#: and ``pcapkit/const/tcp/flags.py`` -- it never defined one, so it is the same +#: shape as the four ``mh/*_flag`` registries, not the five below. Its own +#: ``_missing_`` still does its own 16-bit range check ending in +#: ``super()._missing_(value)``, unchanged by the conversion: that call chain +#: resolves through :mod:`aenum`'s own :class:`~aenum.Flag` machinery either +#: way, since :class:`~pcapkit.corekit.enums.EnumRegistry` never defines +#: ``_missing_`` itself. The remaining five bespoke templates (``ftp/command``, +#: ``ftp/return_code``, ``http/method``, ``http/status_code``, +#: ``pcapng/option_type``) do each define a custom ``__new__`` attaching further +#: attributes, so an unregistered member of theirs would be missing them; they +#: need their own override and stay out of this batch. +CONVERTED = ( + ('pcapkit.const.mh.binding_ack_flag', 'BindingACKFlag', 'pcapkit/const/mh/binding_ack_flag.py'), + ('pcapkit.const.mh.binding_update_flag', 'BindingUpdateFlag', 'pcapkit/const/mh/binding_update_flag.py'), + ('pcapkit.const.mh.handover_ack_flag', 'HandoverACKFlag', 'pcapkit/const/mh/handover_ack_flag.py'), + ('pcapkit.const.mh.handover_initiate_flag', 'HandoverInitiateFlag', 'pcapkit/const/mh/handover_initiate_flag.py'), + ('pcapkit.const.ipv6.extension_header', 'ExtensionHeader', 'pcapkit/const/ipv6/extension_header.py'), + ('pcapkit.const.tcp.flags', 'Flags', 'pcapkit/const/tcp/flags.py'), +) + +#: The crawlers whose bespoke templates were converted, and which must therefore +#: no longer spell a ``get()`` of their own. +CONVERTED_VENDORS = ( + 'pcapkit/vendor/mh/binding_ack_flag.py', + 'pcapkit/vendor/mh/binding_update_flag.py', + 'pcapkit/vendor/mh/handover_ack_flag.py', + 'pcapkit/vendor/mh/handover_initiate_flag.py', + 'pcapkit/vendor/ipv6/extension_header.py', + 'pcapkit/vendor/tcp/flags.py', +) + +#: Every method of the protocol the ruling says must always exist. +PROTOCOL = ('get', 'get_all', 'register', 'register_alias', 'register_aliases', + '_unregistered_member') + + +def _unused_value(cls: 'Any') -> 'int': + """First non-negative integer no member of ``cls`` carries. + + Read from ``_value2member_map_`` rather than by calling ``cls(value)``: + after tier 1 a declared-but-unassigned value resolves to an unregistered + member instead of raising, so a successful call proves nothing about + membership -- the same reason ``register_alias`` itself tests that table. + + """ + for candidate in range(1 << 16): + if candidate not in cls._value2member_map_: + return candidate + raise AssertionError(f'{cls.__name__} has no unused value below 65536') + + +def _purge_member(cls: 'Any', name: 'str') -> 'None': + """Undo an :func:`~aenum.extend_enum` so a test's explicit ``register()`` or + ``register_alias()`` call does not leak into the rest of the suite. + + Mirrors ``tests.const.test_const_enum_no_mint._purge_member``, but leaves + ``_value2member_map_`` alone when the name being dropped was an *alias*: + that entry belongs to the pre-existing member, and removing it would make + the alias case destructive where the mint case's is not. + + """ + member = cls._member_map_.pop(name, None) + if member is None: + return + if name in cls._member_names_: + cls._member_names_.remove(name) + if cls._value2member_map_.get(member.value) is member and member.name == name: + cls._value2member_map_.pop(member.value, None) + + +class GeneratedSourceInheritsTests(unittest.TestCase): + """The generated registries must *inherit* the protocol, not copy it.""" + + def test_converted_const_modules_declare_the_base(self) -> None: + for _, name, relpath in CONVERTED: + with self.subTest(registry=name): + source = (REPO_ROOT / relpath).read_text() + self.assertIn('from pcapkit.corekit.enums import EnumRegistry', source) + self.assertIn(f'class {name}(EnumRegistry, ', source) + + def test_converted_const_modules_carry_no_copy_of_the_protocol(self) -> None: + """A ``def get``/``def register`` left behind would silently shadow the + base class, which is the failure this batch exists to remove.""" + for _, name, relpath in CONVERTED: + source = (REPO_ROOT / relpath).read_text() + for method in PROTOCOL: + with self.subTest(registry=name, method=method): + self.assertNotIn(f'def {method}(', source) + + def test_converted_crawlers_no_longer_spell_their_own_get(self) -> None: + for relpath in CONVERTED_VENDORS: + with self.subTest(vendor=relpath): + source = (REPO_ROOT / relpath).read_text() + self.assertIn('from pcapkit.corekit.enums import EnumRegistry', source) + self.assertNotIn("def get(key: 'int | str'", source) + + +class ProtocolIsInheritedTests(unittest.TestCase): + """All of the protocol must be present, and reached from the base class.""" + + if TYPE_CHECKING: + registries: 'list[Any]' + base: 'Any' + + @classmethod + def setUpClass(cls) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + cls.registries = [ + getattr(importlib.import_module(module_name), class_name) + for module_name, class_name, _ in CONVERTED + ] + # NOTE: resolved from the re-imported module rather than reusing the + # module-scope import. purge_modules() above drops every ``pcapkit`` + # entry from ``sys.modules``, so the freshly imported registries inherit + # a *new* EnumRegistry class object -- identity against the outer one + # would fail for a reason that says nothing about the product. + cls.base = importlib.import_module('pcapkit.corekit.enums').EnumRegistry + cls.addClassCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_base_class_is_in_the_mro(self) -> None: + for registry in self.registries: + with self.subTest(registry=registry.__qualname__): + self.assertIn(self.base, registry.__mro__) + + def test_every_method_resolves_to_the_base_implementation(self) -> None: + for registry in self.registries: + for method in PROTOCOL: + with self.subTest(registry=registry.__qualname__, method=method): + self.assertEqual(getattr(registry, method).__func__, + getattr(self.base, method).__func__) + + def test_mixing_in_the_base_leaves_the_member_type_alone(self) -> None: + """One base for three member types is what a generated fragment could + not do, so the mix-in must not become the member data type itself.""" + class _Int(EnumRegistry, IntEnum): + one = 1 + + class _Flag(EnumRegistry, IntFlag): + two = 2 + + class _Str(EnumRegistry, StrEnum): + three = 'three' + + self.assertIs(_Int._member_type_, int) + self.assertIs(_Flag._member_type_, int) + self.assertIs(_Str._member_type_, str) + self.assertEqual(int(_Int.one), 1) + self.assertEqual(str(_Str.three), 'three') + + +class _PreConversionFlags(IntFlag): + """A reproduction of ``pcapkit.const.tcp.flags.Flags`` exactly as it stood + on commit ``02296b5dd`` (this batch's prior head), before ``tcp/flags`` + joined :data:`CONVERTED`: a plain ``class Flags(IntFlag)`` with its own + hand-copied ``get`` (omitted here -- irrelevant to ``_missing_``) and this + ``_missing_``, copied verbatim. Kept inline rather than fetched from git + history so :class:`TCPFlagsConversionTests` runs offline and the + comparison is exact rather than approximate. + """ + + Reserved_4 = 1 << 4 + Reserved_5 = 1 << 5 + Reserved_6 = 1 << 6 + AE = 1 << 7 + CWR = 1 << 8 + ECE = 1 << 9 + URG = 1 << 10 + ACK = 1 << 11 + PSH = 1 << 12 + RST = 1 << 13 + SYN = 1 << 14 + FIN = 1 << 15 + + @classmethod + def _missing_(cls, value: 'Any') -> 'Any': + if not (isinstance(value, int) and 0 <= value <= 0xFFFF): + raise ValueError(f'{value!r} is not a valid {cls.__name__}') + return super()._missing_(value) + + +def _resolve_or_raise(cls: 'Any', value: 'Any') -> 'Any': + """``(int value, name)`` on success, or the exception type on failure. + + The shared probe :meth:`TCPFlagsConversionTests.test_missing_resolves_identically_to_the_pre_conversion_shape` + runs against both the reference above and the real, converted registry. + + """ + try: + member = cls(value) + except Exception as error: # pylint: disable=broad-except + return type(error) + return (int(member), member.name) + + +class TCPFlagsConversionTests(unittest.TestCase): + """GitHub issue #775's own finding: the PR description's stated reason + for excluding ``tcp/flags`` from this batch -- "each attach extra + attributes in ``__new__``" -- does not hold for it. + ``grep -c 'def __new__'`` reports ``0`` for both + ``pcapkit/vendor/tcp/flags.py`` and ``pcapkit/const/tcp/flags.py``, the + same shape as the four ``mh/*_flag`` registries this batch already + converts, so it belongs in :data:`CONVERTED` rather than in the five + templates excluded for actually defining one. + """ + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_tcp_flags_now_inherits_the_base(self) -> None: + """Fails on ``02296b5dd``, where ``Flags`` is a plain ``IntFlag``. + + Resolves ``EnumRegistry`` freshly from the just-reimported + ``pcapkit.corekit.enums`` rather than the module-scope import above -- + ``setUp`` purged ``pcapkit`` from ``sys.modules``, so ``Flags`` now + inherits a *new* ``EnumRegistry`` class object, and identity against + the stale outer one would fail for a reason that says nothing about + the product (see ``ProtocolIsInheritedTests.setUpClass`` above, which + hits the same trap). + """ + from pcapkit.const.tcp.flags import Flags + + base = importlib.import_module('pcapkit.corekit.enums').EnumRegistry + self.assertIn(base, Flags.__mro__) + + def test_missing_resolves_identically_to_the_pre_conversion_shape(self) -> None: + """The conversion changes *what the class inherits*, not + ``_missing_``'s own logic -- :class:`~pcapkit.corekit.enums.EnumRegistry` + never defines ``_missing_`` itself, so ``super()._missing_(value)`` + inside ``Flags._missing_`` resolves through :mod:`aenum`'s + :class:`~aenum.Flag` machinery exactly as it did when ``Flags`` + inherited from ``IntFlag`` directly. Swept rather than sampled at a + few boundary values, across every declared member, every boundary of + the 16-bit field the range check bounds, and several composites, + so a widened or narrowed range check would be caught here rather + than only in the composite-specific tests in + ``tests/const/test_const_enum_builtin_parity.py``. + """ + from pcapkit.const.tcp.flags import Flags + + probes = [0, 1, 0xF, 0x10, 0xFFFF, 0x10000, -1, -0x10000, + 1 << 70, 0xFFF0] + for member in _PreConversionFlags: + probes.append(int(member)) + for first, second in (('PSH', 'ACK'), ('SYN', 'FIN'), ('RST', 'URG')): + probes.append(int(_PreConversionFlags[first]) | int(_PreConversionFlags[second])) + + for value in probes: + with self.subTest(value=value): + self.assertEqual(_resolve_or_raise(_PreConversionFlags, value), + _resolve_or_raise(Flags, value)) + + def test_tcp_flags_gains_the_protocol_it_never_had(self) -> None: + """On ``02296b5dd`` none of these exist on ``Flags`` at all -- its own + hand-copied ``get`` was the only member of the protocol it carried, + and it had no ``get_all``, ``register``, ``register_alias``, + ``register_aliases`` or ``_unregistered_member`` -- so this raises + :exc:`AttributeError` there and only passes once the conversion lands. + """ + from pcapkit.const.tcp.flags import Flags + + target = list(Flags)[0] + self.assertEqual(Flags.get_all(target.name), (target,)) + + value = _unused_value(Flags) + self.addCleanup(_purge_member, Flags, 'unit_test_tcp_flags_minted') + member = Flags.register(value, 'unit_test_tcp_flags_minted') + self.assertEqual(member.value, value) + + unregistered = Flags._unregistered_member(_unused_value(Flags), 'unit_test_tcp_flags_absent') + self.assertEqual(unregistered.name, 'unit_test_tcp_flags_absent') + self.assertNotIn('unit_test_tcp_flags_absent', Flags.__members__) + + +class GetContractTests(unittest.TestCase): + """*"get is a shortcut for ``[]`` operation and returns the canonical enum."*""" + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_name_and_value_both_resolve_to_the_same_member(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + self.assertIs(ExtensionHeader.get(target.name), target) + self.assertIs(ExtensionHeader.get(target.value), target) + self.assertIs(ExtensionHeader.get(target.name), ExtensionHeader[target.name]) + + def test_an_alias_resolves_to_its_canonical_member(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + self.addCleanup(_purge_member, ExtensionHeader, 'unit_test_canonical') + ExtensionHeader.register_alias(target.value, 'unit_test_canonical') + + self.assertIs(ExtensionHeader.get('unit_test_canonical'), target) + self.assertEqual(ExtensionHeader.get('unit_test_canonical').name, target.name) + + def test_string_miss_without_default_raises_and_does_not_mint(self) -> None: + from pcapkit.const.mh.binding_update_flag import BindingUpdateFlag + + before = len(BindingUpdateFlag.__members__) + with self.assertRaises(KeyError): + BindingUpdateFlag.get('Definitely-Not-A-Member') + self.assertEqual(before, len(BindingUpdateFlag.__members__)) + + def test_string_miss_with_default_falls_back_by_value(self) -> None: + """The normalisation half of this batch: the hand-copied ``get()`` ended + in a bare ``return NAME[key]``, so ``default`` silently applied to + integer keys only.""" + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + before = len(ExtensionHeader.__members__) + + self.assertIs(ExtensionHeader.get('Definitely-Not-A-Member', target.value), target) + self.assertEqual(before, len(ExtensionHeader.__members__)) + + def test_value_miss_without_default_raises(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + before = len(ExtensionHeader.__members__) + with self.assertRaises(ValueError): + ExtensionHeader.get(_unused_value(ExtensionHeader)) + self.assertEqual(before, len(ExtensionHeader.__members__)) + + def test_value_miss_with_default_falls_back(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + before = len(ExtensionHeader.__members__) + + result = ExtensionHeader.get(_unused_value(ExtensionHeader), target.value) + + self.assertIs(result, target) + self.assertEqual(before, len(ExtensionHeader.__members__)) + + def test_get_never_mints_on_any_converted_registry(self) -> None: + for module_name, class_name, _ in CONVERTED: + with self.subTest(registry=class_name): + registry = getattr(importlib.import_module(module_name), class_name) + before = len(registry.__members__) + with self.assertRaises(KeyError): + registry.get('Definitely-Not-A-Member') + self.assertEqual(before, len(registry.__members__)) + + +class GetAllContractTests(unittest.TestCase): + """*"get_all returns all matching enums."*""" + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_a_one_to_one_registry_matches_exactly_one_member(self) -> None: + """An alias is a second *name* for the canonical member, not a second + member, so a registry that maps one key to one member answers with one + entry even after an alias is registered.""" + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + self.assertEqual(ExtensionHeader.get_all(target.value), (target,)) + + self.addCleanup(_purge_member, ExtensionHeader, 'unit_test_all') + ExtensionHeader.register_alias(target.value, 'unit_test_all') + + self.assertEqual(ExtensionHeader.get_all(target.value), (target,)) + self.assertEqual(ExtensionHeader.get_all('unit_test_all'), (target,)) + + def test_get_all_is_present_on_every_converted_registry(self) -> None: + for module_name, class_name, _ in CONVERTED: + with self.subTest(registry=class_name): + registry = getattr(importlib.import_module(module_name), class_name) + target = list(registry)[0] + self.assertEqual(registry.get_all(target.name), (target,)) + + def test_get_all_propagates_a_miss(self) -> None: + from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag + + with self.assertRaises(KeyError): + HandoverACKFlag.get_all('Definitely-Not-A-Member') + + +class RegisterContractTests(unittest.TestCase): + """*"register mints new enum to the class at runtime with specified names."*""" + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_register_adds_a_new_member_under_the_given_name(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + value = _unused_value(ExtensionHeader) + self.addCleanup(_purge_member, ExtensionHeader, 'unit_test_minted') + before = len(ExtensionHeader._member_names_) + + member = ExtensionHeader.register(value, 'unit_test_minted') + + self.assertEqual(member.value, value) + self.assertEqual(member.name, 'unit_test_minted') + self.assertIn(value, ExtensionHeader._value2member_map_) + self.assertEqual(before + 1, len(ExtensionHeader._member_names_)) + + def test_register_on_a_flag_registry_resolves_by_name_and_value(self) -> None: + """Measured on :class:`~aenum.IntFlag`: a value that is the bitwise + composite of existing flags -- which the first unused integer often is -- + registers as a *composite* pseudo-member, so ``_member_names_`` does not + grow even though the name and value both resolve. That is + :mod:`aenum`'s own flag semantics rather than anything this base does, + so the assertion here is what actually holds for a flag registry.""" + from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag + + value = _unused_value(HandoverACKFlag) + self.addCleanup(_purge_member, HandoverACKFlag, 'unit_test_minted') + + member = HandoverACKFlag.register(value, 'unit_test_minted') + + self.assertEqual(member.value, value) + self.assertIn(value, HandoverACKFlag._value2member_map_) + self.assertIn('unit_test_minted', HandoverACKFlag.__members__) + + def test_register_over_a_taken_name_raises_value_error(self) -> None: + """:mod:`aenum` reports a name collision as :exc:`TypeError`; the base + translates it so one call has one failure type.""" + from pcapkit.const.mh.handover_ack_flag import HandoverACKFlag + + target = list(HandoverACKFlag)[0] + before = len(HandoverACKFlag.__members__) + + with self.assertRaises(ValueError): + HandoverACKFlag.register(_unused_value(HandoverACKFlag), target.name) + + self.assertEqual(before, len(HandoverACKFlag.__members__)) + + def test_register_over_a_taken_value_raises_value_error_and_does_not_alias(self) -> None: + """The guard this batch adds: :func:`~aenum.extend_enum` does not mint + anything for a value that already has a member -- :mod:`aenum` treats + that as a request to *alias* the existing member under the caller's + name instead, silently. Without a guard, ``register(existing_value, + 'TOTALLY_NEW_NAME')`` returns the *existing* member (``.name`` still + the original), makes ``'TOTALLY_NEW_NAME'`` reachable in + ``__members__`` pointing at it, and mints nothing -- reachable under + the wrong method, unannounced, and contradicting the method's own + docstring contract that ``register`` is what mints and nothing else + does so silently. Reproduced against this exact shape on + ``pcapkit.const.reg.apptype.apptype.TransportProtocol`` (a different, + non-:class:`~pcapkit.corekit.enums.EnumRegistry` registry, since + that one is not gated the same way) before this guard existed: + ``TransportProtocol.register(6, 'TOTALLY_NEW_NAME')`` returned + ``TransportProtocol.tcp`` unchanged and minted nothing. This pins the + fix on the base class every :data:`CONVERTED` registry actually uses. + """ + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + names_before = list(ExtensionHeader._member_names_) + members_before = dict(ExtensionHeader.__members__) + + with self.assertRaises(ValueError) as caught: + ExtensionHeader.register(target.value, 'TOTALLY_NEW_NAME') + + self.assertIn(str(target.value), str(caught.exception)) + self.assertIn(target.name, str(caught.exception)) + self.assertIn('register_alias', str(caught.exception)) + # Nothing minted, and the wrong name never became reachable at all -- + # the failure mode this guard exists to rule out. + self.assertEqual(names_before, list(ExtensionHeader._member_names_)) + self.assertEqual(members_before, dict(ExtensionHeader.__members__)) + self.assertNotIn('TOTALLY_NEW_NAME', ExtensionHeader.__members__) + # And the pre-existing member is exactly as it was -- not renamed, not + # replaced. + self.assertIs(ExtensionHeader(target.value), target) + self.assertEqual(target.name, ExtensionHeader(target.value).name) + + def test_register_over_a_taken_value_on_a_flag_registry_also_refuses(self) -> None: + """The same guard, on an :class:`~aenum.IntFlag` registry: value + collision is checked the same way regardless of member type, since + both share :meth:`~pcapkit.corekit.enums.EnumRegistry._extend`.""" + from pcapkit.const.mh.binding_ack_flag import BindingACKFlag + + target = list(BindingACKFlag)[0] + before = len(BindingACKFlag.__members__) + + with self.assertRaises(ValueError): + BindingACKFlag.register(target.value, 'TOTALLY_NEW_FLAG_NAME') + + self.assertEqual(before, len(BindingACKFlag.__members__)) + self.assertNotIn('TOTALLY_NEW_FLAG_NAME', BindingACKFlag.__members__) + + def test_register_alias_still_aliases_after_the_value_guard(self) -> None: + """:meth:`register_alias` depends on :meth:`register` accepting an + already-registered value -- that dependency moved to the shared, + ungated :meth:`~pcapkit.corekit.enums.EnumRegistry._extend` when this + guard was added, so this pins that the move did not also gate the + path :meth:`register_alias` needs. A naive guard placed directly in + the body :meth:`register` calls would make every alias registration + raise the exact error this test's sibling above checks for, rather + than aliasing. Deliberately on ``ExtensionHeader`` rather than + ``Flags``: this registry already inherited + :class:`~pcapkit.corekit.enums.EnumRegistry` before this change, so + this is a regression pin on the internal refactor and holds on both + the prior head and this one -- unlike this class's ``Flags``-based + siblings above, which pin the guard itself and so only hold once + ``tcp/flags`` has joined :data:`CONVERTED` too.""" + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + self.addCleanup(_purge_member, ExtensionHeader, 'unit_test_alias_after_guard') + + result = ExtensionHeader.register_alias(target.value, 'unit_test_alias_after_guard') + + self.assertIs(result, target) + self.assertIs(ExtensionHeader['unit_test_alias_after_guard'], target) + + +class RegisterAliasContractTests(unittest.TestCase): + """*"register_alias(es) adds additional alias(es) to a given enum's mapping."* + + And, on whether an enum must be given: *"actually i think it should always be + for an existing member"*. + + """ + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_alias_adds_a_name_not_a_member(self) -> None: + from pcapkit.const.ipv6.extension_header import ExtensionHeader + + target = list(ExtensionHeader)[0] + self.addCleanup(_purge_member, ExtensionHeader, 'unit_test_alias') + names_before = list(ExtensionHeader._member_names_) + members_before = len(ExtensionHeader.__members__) + + result = ExtensionHeader.register_alias(target.value, 'unit_test_alias') + + self.assertIs(result, target) + self.assertIs(ExtensionHeader['unit_test_alias'], target) + self.assertEqual(names_before, list(ExtensionHeader._member_names_)) + self.assertEqual(members_before + 1, len(ExtensionHeader.__members__)) + + def test_alias_for_an_unregistered_value_is_refused(self) -> None: + from pcapkit.const.mh.binding_ack_flag import BindingACKFlag + + value = _unused_value(BindingACKFlag) + before = len(BindingACKFlag.__members__) + + with self.assertRaises(ValueError) as caught: + BindingACKFlag.register_alias(value, 'unit_test_alias') + + self.assertIn('is not a registered BindingACKFlag', str(caught.exception)) + self.assertEqual(before, len(BindingACKFlag.__members__)) + self.assertNotIn('unit_test_alias', BindingACKFlag.__members__) + + def test_alias_over_a_taken_name_raises_value_error(self) -> None: + from pcapkit.const.mh.binding_ack_flag import BindingACKFlag + + target = list(BindingACKFlag)[0] + before = len(BindingACKFlag.__members__) + + with self.assertRaises(ValueError): + BindingACKFlag.register_alias(target.value, target.name) + + self.assertEqual(before, len(BindingACKFlag.__members__)) + + def test_register_aliases_adds_several(self) -> None: + from pcapkit.const.mh.handover_initiate_flag import HandoverInitiateFlag + + target = list(HandoverInitiateFlag)[0] + for name in ('unit_test_a', 'unit_test_b'): + self.addCleanup(_purge_member, HandoverInitiateFlag, name) + names_before = list(HandoverInitiateFlag._member_names_) + + result = HandoverInitiateFlag.register_aliases( + target.value, 'unit_test_a', 'unit_test_b') + + self.assertEqual(result, (target, target)) + self.assertIs(HandoverInitiateFlag['unit_test_a'], target) + self.assertIs(HandoverInitiateFlag['unit_test_b'], target) + self.assertEqual(names_before, list(HandoverInitiateFlag._member_names_)) + + +class UnregisteredMemberTests(unittest.TestCase): + """``_unregistered_member`` must stay outside the lookup tables, for both + member types the base serves.""" + + def setUp(self) -> None: + snapshot = snapshot_modules(ISOLATED_PREFIXES) + purge_modules(['pcapkit']) + self.addCleanup(restore_modules, snapshot, ISOLATED_PREFIXES) + + def test_int_valued_registries(self) -> None: + for module_name, class_name, _ in CONVERTED: + with self.subTest(registry=class_name): + registry = getattr(importlib.import_module(module_name), class_name) + value = _unused_value(registry) + + member = registry._unregistered_member(value, 'unit_test_absent') + + self.assertEqual(member.value, value) + self.assertEqual(member.name, 'unit_test_absent') + self.assertNotIn(value, registry._value2member_map_) + self.assertNotIn('unit_test_absent', registry.__members__) + + def test_str_valued_registries(self) -> None: + """``cls._member_type_.__new__`` is what generalises this beyond ``int`` + -- the generated fragment it replaces hardcoded ``int.__new__``, so the + five :class:`~aenum.StrEnum` registries could not have shared it.""" + class _Str(EnumRegistry, StrEnum): + known = 'known' + + member = _Str._unregistered_member('absent', 'unit_test_absent') + + self.assertEqual(member.value, 'absent') + self.assertEqual(str(member), 'absent') + self.assertEqual(member.name, 'unit_test_absent') + self.assertNotIn('absent', _Str._value2member_map_) + self.assertNotIn('unit_test_absent', _Str.__members__) + + +if __name__ == '__main__': + unittest.main()