Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions pcapkit/const/pcapng/option_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,56 @@ def register(cls, value: 'int', name: 'str') -> 'OptionType':
except TypeError as error:
raise ValueError(str(error)) from error

@classmethod
def register_alias(cls, value: 'int', name: 'str') -> 'OptionType':
"""Add ``name`` as a further name for the member at option code ``value``.

Overrides :meth:`~pcapkit.corekit.enum.EnumRegistry.register_alias`
for the same reason as :meth:`register`: the base looks the
:class:`int` code up in :attr:`~aenum.Enum._value2member_map_`, whose
keys are the formatted ``'{name} [{value}]'`` strings, so it could
never find one.

The member is resolved as :meth:`get` resolves ``value`` with
``namespace`` set to ``name``'s prefix: that namespace overlaid on
``opt``. So ``register_alias(2, 'if_label')`` aliases ``if_name``,
while ``register_alias(1, 'if_note')`` aliases ``opt_comment``, since
``if`` assigns no code 1 of its own. The alias adds a name only; the
member keeps its own name and namespace.

Args:
value: Option code of the existing member to alias.
name: Alias to add for it, led by the namespace to resolve
``value`` in.

Returns:
The existing member, now reachable under ``name`` as well.

Raises:
ValueError: If neither ``name``'s namespace nor ``opt`` assigns
``value``, or if ``name`` is already taken.

"""
namespace = name.split('_', maxsplit=1)[0]
member = cls.__members_ns__.get(namespace, {}).get(value)
if member is None:
member = cls.__members_ns__.get('opt', {}).get(value)
if member is None:
raise ValueError(f'{value!r} is not a registered {cls.__name__} in namespace '
f'{namespace!r} or opt; use {cls.__name__}.register() to mint one')

# NOTE: :func:`~aenum.extend_enum` builds a throwaway member through
# :meth:`__new__` to learn its value, and :meth:`__new__` files that
# throwaway in :attr:`__members_ns__` over ``member``. The alias
# resolves to ``member``, so ``member`` is put back.
member_ns = member.opt_name.split('_', maxsplit=1)[0]
try:
return extend_enum(cls, name, value, member.opt_name)
except TypeError as error:
raise ValueError(str(error)) from error
finally:
cls.__members_ns__[member_ns][value] = member

@staticmethod
def get(key: 'int | str', default: 'int' = -1, *, namespace: 'str' = 'opt') -> 'OptionType':
"""Backport support for original codes.
Expand Down
50 changes: 50 additions & 0 deletions pcapkit/vendor/pcapng/option_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,56 @@ def register(cls, value: 'int', name: 'str') -> '{NAME}':
except TypeError as error:
raise ValueError(str(error)) from error

@classmethod
def register_alias(cls, value: 'int', name: 'str') -> '{NAME}':
"""Add ``name`` as a further name for the member at option code ``value``.

Overrides :meth:`~pcapkit.corekit.enum.EnumRegistry.register_alias`
for the same reason as :meth:`register`: the base looks the
:class:`int` code up in :attr:`~aenum.Enum._value2member_map_`, whose
keys are the formatted ``'{{name}} [{{value}}]'`` strings, so it could
never find one.

The member is resolved as :meth:`get` resolves ``value`` with
``namespace`` set to ``name``'s prefix: that namespace overlaid on
``opt``. So ``register_alias(2, 'if_label')`` aliases ``if_name``,
while ``register_alias(1, 'if_note')`` aliases ``opt_comment``, since
``if`` assigns no code 1 of its own. The alias adds a name only; the
member keeps its own name and namespace.

Args:
value: Option code of the existing member to alias.
name: Alias to add for it, led by the namespace to resolve
``value`` in.

Returns:
The existing member, now reachable under ``name`` as well.

Raises:
ValueError: If neither ``name``'s namespace nor ``opt`` assigns
``value``, or if ``name`` is already taken.

"""
namespace = name.split('_', maxsplit=1)[0]
member = cls.__members_ns__.get(namespace, {{}}).get(value)
if member is None:
member = cls.__members_ns__.get('opt', {{}}).get(value)
if member is None:
raise ValueError(f'{{value!r}} is not a registered {{cls.__name__}} in namespace '
f'{{namespace!r}} or opt; use {{cls.__name__}}.register() to mint one')

# NOTE: :func:`~aenum.extend_enum` builds a throwaway member through
# :meth:`__new__` to learn its value, and :meth:`__new__` files that
# throwaway in :attr:`__members_ns__` over ``member``. The alias
# resolves to ``member``, so ``member`` is put back.
member_ns = member.opt_name.split('_', maxsplit=1)[0]
try:
return extend_enum(cls, name, value, member.opt_name)
except TypeError as error:
raise ValueError(str(error)) from error
finally:
cls.__members_ns__[member_ns][value] = member

@staticmethod
def get(key: 'int | str', default: 'int' = -1, *, namespace: 'str' = 'opt') -> '{NAME}':
"""Backport support for original codes.
Expand Down
94 changes: 94 additions & 0 deletions tests/const/test_const_option_type_alias_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""``OptionType.register_alias`` adds a name for an existing option code.

GitHub issue #1330: :meth:`OptionType.register_alias
<pcapkit.const.pcapng.option_type.OptionType.register_alias>` fell through to
the base, which tests the :class:`int` code against
:attr:`~aenum.Enum._value2member_map_`. That table is keyed by the formatted
``'{name} [{value}]'`` strings, so every call raised ``ValueError: … is not a
registered OptionType``.

The alias resolves ``value`` in the alias name's namespace overlaid on ``opt``,
as :meth:`~pcapkit.const.pcapng.option_type.OptionType.get` does.

The registry is imported inside each test, after
:func:`~tests._support.reimport_once_per_class`, and the class's private import
is restored afterwards, so the aliases added here do not leak. The import is
shared by the tests of the class, so each test uses its own alias names.

"""

import unittest

from tests._support import reimport_once_per_class


class TestOptionTypeRegisterAlias(unittest.TestCase):
"""GitHub issue #1330."""

def setUp(self) -> None:
reimport_once_per_class(self, restore=True)

def test_alias_in_opt_resolves_to_the_member(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

count = len(list(OptionType))
member = OptionType.register_alias(1, 'opt_note_1330')
self.assertIs(member, OptionType.opt_comment)
self.assertIs(getattr(OptionType, 'opt_note_1330'), OptionType.opt_comment)
self.assertIs(OptionType.__members__['opt_note_1330'], OptionType.opt_comment)
self.assertIs(OptionType.get('opt_note_1330'), OptionType.opt_comment)
# An alias adds a name, not a member, and the member keeps its own name.
self.assertEqual(len(list(OptionType)), count)
self.assertEqual(member.name, 'opt_comment')
self.assertEqual(member.opt_name, 'opt_comment')

def test_alias_leaves_the_namespace_table_on_the_member(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

OptionType.register_alias(2, 'if_label_1330')
self.assertIs(OptionType.__members_ns__['if'][2], OptionType.if_name)
self.assertIs(OptionType.get(2, namespace='if'), OptionType.if_name)
self.assertIs(getattr(OptionType, 'if_label_1330'), OptionType.if_name)

def test_alias_namespace_falls_back_to_opt(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

member = OptionType.register_alias(1, 'if_note_1330')
self.assertIs(member, OptionType.opt_comment)
self.assertNotIn(1, OptionType.__members_ns__['if'])
self.assertIs(OptionType.__members_ns__['opt'][1], OptionType.opt_comment)

def test_alias_namespace_takes_precedence_over_opt(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

# Code 2 is ``epb_flags`` in ``epb`` and ``if_name`` in ``if``.
self.assertIs(OptionType.register_alias(2, 'epb_fl_1330'), OptionType.epb_flags)
self.assertIs(OptionType.__members_ns__['epb'][2], OptionType.epb_flags)
self.assertIs(OptionType.__members_ns__['if'][2], OptionType.if_name)

def test_register_aliases_adds_each_name(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

members = OptionType.register_aliases(1, 'opt_a_1330', 'opt_b_1330')
self.assertEqual(members, (OptionType.opt_comment, OptionType.opt_comment))
self.assertIs(OptionType.__members__['opt_b_1330'], OptionType.opt_comment)

def test_unassigned_code_is_refused(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

with self.assertRaisesRegex(ValueError, 'is not a registered OptionType'):
OptionType.register_alias(0x7ABC, 'if_missing_1330')
self.assertNotIn('if_missing_1330', OptionType.__members__)

def test_taken_name_is_refused(self) -> None:
from pcapkit.const.pcapng.option_type import OptionType

with self.assertRaises(ValueError):
OptionType.register_alias(2, 'opt_comment')
self.assertIs(OptionType.__members_ns__['opt'][1], OptionType.opt_comment)
self.assertIs(OptionType.__members_ns__['if'][2], OptionType.if_name)


if __name__ == '__main__':
unittest.main()
Loading