diff --git a/docs/source/pcapkit/const/reg.rst b/docs/source/pcapkit/const/reg.rst index 95b3cd2933..9859e04244 100644 --- a/docs/source/pcapkit/const/reg.rst +++ b/docs/source/pcapkit/const/reg.rst @@ -105,6 +105,13 @@ registry is one enumeration per transport protocol sharing a memberless base. :class:`int` to its port number, and compares, orders and hashes as that port. +.. versionchanged:: 1.6.0 + + :meth:`AppType.get() ` takes + the ``default`` of :meth:`~pcapkit.corekit.enum.EnumLookup.get`, and a miss + raises :exc:`~pcapkit.utilities.exceptions.EnumValueError`, still a + :exc:`ValueError`, rather than a builtin one (:issue:`2545`). + .. autoclass:: pcapkit.const.reg.apptype.apptype.AppType :members: :undoc-members: diff --git a/pcapkit/const/reg/apptype/apptype.py b/pcapkit/const/reg/apptype/apptype.py index 13c8c3b1e3..48867f45c3 100644 --- a/pcapkit/const/reg/apptype/apptype.py +++ b/pcapkit/const/reg/apptype/apptype.py @@ -99,9 +99,10 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco name miss reaches the caller as :exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base. :class:`AppType` and its per-transport registries raise - :exc:`ValueError` from their own ``get()`` because it takes an - :class:`int` port and never accepts a name; that is not a name-miss - policy, so it sets none for this class to follow. The + :exc:`~pcapkit.utilities.exceptions.EnumValueError` from their own + ``get()`` because it takes an :class:`int` port and never accepts a + name; that is not a name-miss policy, so it sets none for this class + to follow. The maintainer's ruling given in review of the work for :issue:`808`, that :class:`TransportProtocol` is not to be extended at all, holds either way: the refusal is a refusal and mints nothing, whichever exception @@ -2596,12 +2597,27 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ f'{cls.__name__}') @classmethod - def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=arguments-differ + def get(cls, key: 'int', default: 'Any' = NO_DEFAULT, *, proto: 'TransportProtocol | str | int' = TransportProtocol.undefined) -> 'AppType': """Resolve port ``key`` to the member for its canonical service. + Takes the ``default`` :meth:`~pcapkit.corekit.enum.EnumLookup.get` + advertises, since an override honours the base's signature rather than + suppressing the mismatch (:issue:`935`), and answers a miss as the base + does, from :mod:`pcapkit.utilities.exceptions` (:issue:`923`). + Args: key: Port number to look up. + default: An already-registered value to fall back to when ``key`` + resolves to nothing: a member's ``value``, such as + ``'http [80 - tcp]'``, and not a port or a member, which hashes + as its port. Resolved as + :meth:`~pcapkit.corekit.enum.EnumLookup.get` resolves its own, + through ``_value2member_map_`` and so never minting, in the + registry ``key`` was dispatched to, or else in the class called + on, which on :class:`AppType` itself holds no members. + :data:`~pcapkit.corekit.sentinels.NO_DEFAULT`, the default, means + *no default*. proto: Transport protocol carrying ``key``. Selects the registry to search when called on :class:`AppType` itself, which holds no members; ignored when called on one of those registries, each of @@ -2628,53 +2644,60 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument registry's own transport (:issue:`2048`). Raises: - ValueError: If called on a class that holds no members, i.e. on - :class:`AppType` itself, with a ``proto`` naming no registry to - delegate to. Also for a ``key`` that is not a port number, since - this registry resolves ports and not service names -- including - one outside ``0..65535``, whose rejection by :meth:`_missing_` this - method propagates rather than minting over, so that ``get`` is - never more permissive than ``AppType(...)``. Also covers a - ``proto`` int that matches no registry at all, composite or - not -- looked up as the whole value it is rather than - decoded into the registries its bits would each name, so it - is refused only when that whole value itself names none of - them, not simply for being composite -- see - :meth:`_dispatch`. + EnumValueError: If ``key`` resolves to nothing and there is no + usable ``default``: a ``key`` that is not a port number, since + this registry resolves ports and not service names, including + one outside ``0..65535``, which :meth:`_missing_` rejects rather + than minting over, so that ``get`` is never more permissive than + ``AppType(...)``; or, on :class:`AppType` itself, a ``proto`` + naming no registry to delegate to, composite or not (see + :meth:`_dispatch`). A :exc:`ValueError`, and quiet, as from the + base. + EnumKeyError: If, on :class:`AppType` itself, ``proto`` is a name no + :class:`TransportProtocol` member carries, from + :meth:`TransportProtocol.get`. """ - owner = cls._dispatch(key, proto) + owner = cls + try: + owner = cls._dispatch(key, proto) - matched = owner.__registry__.getlist(key) # type: ignore[union-attr] - if matched: - canonical = owner.__canonical__.get(key) - if canonical is not None: - for member in matched: - if member.svc == canonical: - return member - # NOTE: IANA's oldest row, for a port whose collision postdates - # :data:`__canonical__` -- including one ``aenum.extend_enum`` - # created, which must not displace what the registry already - # answered with. - return matched[0] + matched = owner.__registry__.getlist(key) # type: ignore[union-attr] + if matched: + canonical = owner.__canonical__.get(key) + if canonical is not None: + for member in matched: + if member.svc == canonical: + return member + # NOTE: IANA's oldest row, for a port whose collision postdates + # :data:`__canonical__` -- including one ``aenum.extend_enum`` + # created, which must not displace what the registry already + # answered with. + return matched[0] - # NOTE: :meth:`_missing_` answers :obj:`None` for a port it holds no row - # for, and that answer does not mint (GitHub issue #860), following the - # owner's ruling there that ``get`` should not mint because it lacks the - # information to create a new member: only IANA-registered values are - # legitimate, and ``register`` is the proper way to add one. That ruling - # was given for the FEATCode, Command and Method branches, and covers - # this family under the same issue. A port outside ``0..65535`` *raises* - # rather than reaching here at all. Minting on the ``None`` answer would - # name a member ``PORT_{key}_{transport}`` -- GitHub issue #758's - # defect, which also minted ``PORT_999999_tcp`` and ``PORT_-1_tcp``, the - # latter a name no attribute access could reach -- so, as inside - # :meth:`_missing_` itself, the port gets an unregistered member - # instead: neither is a value anyone asked to be named. A port IANA - # lists on other transports only is in no span on this one: 8011, - # reserved on TCP and ``bier-echo`` on UDP, answers ``unknown`` on DCCP - # and SCTP (#2048). - ret = owner._missing_(key) + # NOTE: :meth:`_missing_` answers :obj:`None` for a port it holds no row + # for, and that answer does not mint (GitHub issue #860), following the + # owner's ruling there that ``get`` should not mint because it lacks the + # information to create a new member: only IANA-registered values are + # legitimate, and ``register`` is the proper way to add one. That ruling + # was given for the FEATCode, Command and Method branches, and covers + # this family under the same issue. A port outside ``0..65535`` *raises* + # rather than reaching here at all. Minting on the ``None`` answer would + # name a member ``PORT_{key}_{transport}`` -- GitHub issue #758's + # defect, which also minted ``PORT_999999_tcp`` and ``PORT_-1_tcp``, the + # latter a name no attribute access could reach -- so, as inside + # :meth:`_missing_` itself, the port gets an unregistered member + # instead: neither is a value anyone asked to be named. A port IANA + # lists on other transports only is in no span on this one: 8011, + # reserved on TCP and ``bier-echo`` on UDP, answers ``unknown`` on DCCP + # and SCTP (#2048). + ret = owner._missing_(key) + except ValueError as error: + # NOTE: answered as EnumLookup.get answers a value miss (#923, #2545), + # with ``default`` from the registry dispatched to, else from ``cls``. + if default is NO_DEFAULT or default not in owner._value2member_map_: + raise EnumValueError(str(error), quiet=True) from error + return owner._value2member_map_[default] if ret is None: ret = owner._unregistered_member(key, 'unknown', owner.__transport__) return ret diff --git a/pcapkit/vendor/reg/apptype/apptype.py b/pcapkit/vendor/reg/apptype/apptype.py index 2d6083567a..1af0e01ab1 100644 --- a/pcapkit/vendor/reg/apptype/apptype.py +++ b/pcapkit/vendor/reg/apptype/apptype.py @@ -191,9 +191,10 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco name miss reaches the caller as :exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base. :class:`AppType` and its per-transport registries raise - :exc:`ValueError` from their own ``get()`` because it takes an - :class:`int` port and never accepts a name; that is not a name-miss - policy, so it sets none for this class to follow. The + :exc:`~pcapkit.utilities.exceptions.EnumValueError` from their own + ``get()`` because it takes an :class:`int` port and never accepts a + name; that is not a name-miss policy, so it sets none for this class + to follow. The maintainer's ruling given in review of the work for :issue:`808`, that :class:`TransportProtocol` is not to be extended at all, holds either way: the refusal is a refusal and mints nothing, whichever exception @@ -550,12 +551,27 @@ def _dispatch(cls, key: 'int', proto: 'TransportProtocol | str | int') -> 'Type[ f'{{cls.__name__}}') @classmethod - def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=arguments-differ + def get(cls, key: 'int', default: 'Any' = NO_DEFAULT, *, proto: 'TransportProtocol | str | int' = TransportProtocol.undefined) -> '{NAME}': """Resolve port ``key`` to the member for its canonical service. + Takes the ``default`` :meth:`~pcapkit.corekit.enum.EnumLookup.get` + advertises, since an override honours the base's signature rather than + suppressing the mismatch (:issue:`935`), and answers a miss as the base + does, from :mod:`pcapkit.utilities.exceptions` (:issue:`923`). + Args: key: Port number to look up. + default: An already-registered value to fall back to when ``key`` + resolves to nothing: a member's ``value``, such as + ``'http [80 - tcp]'``, and not a port or a member, which hashes + as its port. Resolved as + :meth:`~pcapkit.corekit.enum.EnumLookup.get` resolves its own, + through ``_value2member_map_`` and so never minting, in the + registry ``key`` was dispatched to, or else in the class called + on, which on :class:`{NAME}` itself holds no members. + :data:`~pcapkit.corekit.sentinels.NO_DEFAULT`, the default, means + *no default*. proto: Transport protocol carrying ``key``. Selects the registry to search when called on :class:`{NAME}` itself, which holds no members; ignored when called on one of those registries, each of @@ -582,53 +598,60 @@ def get(cls, key: 'int', *, # type: ignore[override] # pylint: disable=argument registry's own transport (:issue:`2048`). Raises: - ValueError: If called on a class that holds no members, i.e. on - :class:`{NAME}` itself, with a ``proto`` naming no registry to - delegate to. Also for a ``key`` that is not a port number, since - this registry resolves ports and not service names -- including - one outside ``0..65535``, whose rejection by :meth:`_missing_` this - method propagates rather than minting over, so that ``get`` is - never more permissive than ``{NAME}(...)``. Also covers a - ``proto`` int that matches no registry at all, composite or - not -- looked up as the whole value it is rather than - decoded into the registries its bits would each name, so it - is refused only when that whole value itself names none of - them, not simply for being composite -- see - :meth:`_dispatch`. + EnumValueError: If ``key`` resolves to nothing and there is no + usable ``default``: a ``key`` that is not a port number, since + this registry resolves ports and not service names, including + one outside ``0..65535``, which :meth:`_missing_` rejects rather + than minting over, so that ``get`` is never more permissive than + ``{NAME}(...)``; or, on :class:`{NAME}` itself, a ``proto`` + naming no registry to delegate to, composite or not (see + :meth:`_dispatch`). A :exc:`ValueError`, and quiet, as from the + base. + EnumKeyError: If, on :class:`{NAME}` itself, ``proto`` is a name no + :class:`TransportProtocol` member carries, from + :meth:`TransportProtocol.get`. """ - owner = cls._dispatch(key, proto) - - matched = owner.__registry__.getlist(key) # type: ignore[union-attr] - if matched: - canonical = owner.__canonical__.get(key) - if canonical is not None: - for member in matched: - if member.svc == canonical: - return member - # NOTE: IANA's oldest row, for a port whose collision postdates - # :data:`__canonical__` -- including one ``aenum.extend_enum`` - # created, which must not displace what the registry already - # answered with. - return matched[0] - - # NOTE: :meth:`_missing_` answers :obj:`None` for a port it holds no row - # for, and that answer does not mint (GitHub issue #860), following the - # owner's ruling there that ``get`` should not mint because it lacks the - # information to create a new member: only IANA-registered values are - # legitimate, and ``register`` is the proper way to add one. That ruling - # was given for the FEATCode, Command and Method branches, and covers - # this family under the same issue. A port outside ``0..65535`` *raises* - # rather than reaching here at all. Minting on the ``None`` answer would - # name a member ``PORT_{{key}}_{{transport}}`` -- GitHub issue #758's - # defect, which also minted ``PORT_999999_tcp`` and ``PORT_-1_tcp``, the - # latter a name no attribute access could reach -- so, as inside - # :meth:`_missing_` itself, the port gets an unregistered member - # instead: neither is a value anyone asked to be named. A port IANA - # lists on other transports only is in no span on this one: 8011, - # reserved on TCP and ``bier-echo`` on UDP, answers ``unknown`` on DCCP - # and SCTP (#2048). - ret = owner._missing_(key) + owner = cls + try: + owner = cls._dispatch(key, proto) + + matched = owner.__registry__.getlist(key) # type: ignore[union-attr] + if matched: + canonical = owner.__canonical__.get(key) + if canonical is not None: + for member in matched: + if member.svc == canonical: + return member + # NOTE: IANA's oldest row, for a port whose collision postdates + # :data:`__canonical__` -- including one ``aenum.extend_enum`` + # created, which must not displace what the registry already + # answered with. + return matched[0] + + # NOTE: :meth:`_missing_` answers :obj:`None` for a port it holds no row + # for, and that answer does not mint (GitHub issue #860), following the + # owner's ruling there that ``get`` should not mint because it lacks the + # information to create a new member: only IANA-registered values are + # legitimate, and ``register`` is the proper way to add one. That ruling + # was given for the FEATCode, Command and Method branches, and covers + # this family under the same issue. A port outside ``0..65535`` *raises* + # rather than reaching here at all. Minting on the ``None`` answer would + # name a member ``PORT_{{key}}_{{transport}}`` -- GitHub issue #758's + # defect, which also minted ``PORT_999999_tcp`` and ``PORT_-1_tcp``, the + # latter a name no attribute access could reach -- so, as inside + # :meth:`_missing_` itself, the port gets an unregistered member + # instead: neither is a value anyone asked to be named. A port IANA + # lists on other transports only is in no span on this one: 8011, + # reserved on TCP and ``bier-echo`` on UDP, answers ``unknown`` on DCCP + # and SCTP (#2048). + ret = owner._missing_(key) + except ValueError as error: + # NOTE: answered as EnumLookup.get answers a value miss (#923, #2545), + # with ``default`` from the registry dispatched to, else from ``cls``. + if default is NO_DEFAULT or default not in owner._value2member_map_: + raise EnumValueError(str(error), quiet=True) from error + return owner._value2member_map_[default] if ret is None: ret = owner._unregistered_member(key, 'unknown', owner.__transport__) return ret diff --git a/tests/const/test_const_apptype_get_default_2545_unit.py b/tests/const/test_const_apptype_get_default_2545_unit.py new file mode 100644 index 0000000000..4e18965050 --- /dev/null +++ b/tests/const/test_const_apptype_get_default_2545_unit.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +"""``AppType.get`` takes the base's ``default`` and raises the base's exceptions. + +GitHub issue #2545. Without the fix, +:meth:`~pcapkit.const.reg.apptype.apptype.AppType.get` is ``get(cls, key, *, +proto=...)`` under a ``# type: ignore[override]`` and ``arguments-differ`` +pair, so ``TCP.get(80, 'x')`` raises :exc:`TypeError` where +:meth:`~pcapkit.corekit.enum.EnumLookup.get` takes ``(key, default)``; and a +miss raises a builtin :exc:`ValueError` where the base raises +:exc:`~pcapkit.utilities.exceptions.EnumValueError`. The registry-protocol +convention rules out both: an override honours the signature the base +advertises (#935), and a failed lookup raises from +:mod:`pcapkit.utilities.exceptions` (#923). + +Each test fails without the fix: the signature test on the missing parameter, +those passing ``default`` on :exc:`TypeError`, and the miss tests on the +builtin :exc:`ValueError`. + +""" +from __future__ import annotations + +import inspect +import unittest + +#: Ports outside ``0``-``65535``, on both sides of the range. +OUT_OF_RANGE = (-1, 65536, 99999) + +#: Keys that are not a port number at all, a service name among them. +NOT_A_PORT = ('http', None, 1.5) + + +def _registries() -> 'tuple[type, ...]': + from pcapkit.const.reg.apptype import DCCP, SCTP, TCP, UDP + + return TCP, UDP, SCTP, DCCP + + +class AppTypeGetSignature2545Tests(unittest.TestCase): + """``get`` accepts every argument the base's ``get`` accepts.""" + + def test_the_bases_parameters_lead_the_signature(self) -> None: + from pcapkit.const.reg.apptype import AppType + from pcapkit.corekit.enum import EnumLookup + + base = list(inspect.signature(EnumLookup.get).parameters.values()) + override = list(inspect.signature(AppType.get).parameters.values()) + self.assertEqual([(param.name, param.kind, param.default) for param in base], + [(param.name, param.kind, param.default) + for param in override[:len(base)]]) + self.assertEqual([(param.name, param.kind) for param in override[len(base):]], + [('proto', inspect.Parameter.KEYWORD_ONLY)]) + + def test_a_two_argument_call_resolves_as_the_one_argument_call(self) -> None: + for registry in _registries(): + member = next(iter(registry)) # type: ignore[call-overload] + with self.subTest(registry=registry.__name__, port=member.port): + self.assertIs(registry.get(member.port, 'x'), # type: ignore[attr-defined] + registry.get(member.port)) # type: ignore[attr-defined] + self.assertIs(registry.get(member.port, default='x'), # type: ignore[attr-defined] + registry.get(member.port)) # type: ignore[attr-defined] + + +class AppTypeGetDefault2545Tests(unittest.TestCase): + """A miss answers with ``default`` as the base's would.""" + + def test_a_miss_falls_back_to_a_registered_value(self) -> None: + for registry in _registries(): + member = next(iter(registry)) # type: ignore[call-overload] + for key in OUT_OF_RANGE + NOT_A_PORT: + with self.subTest(registry=registry.__name__, key=key): + self.assertIs(registry.get(key, member.value), # type: ignore[attr-defined] + member) + + def test_the_base_registry_falls_back_in_the_registry_proto_selects(self) -> None: + from pcapkit.const.reg.apptype import AppType + + for registry in _registries(): + member = next(iter(registry)) # type: ignore[call-overload] + with self.subTest(registry=registry.__name__): + self.assertIs(AppType.get(99999, member.value, proto=registry.__transport__), + member) + + def test_an_unusable_default_raises_the_miss(self) -> None: + """Neither a port nor a member is a value, and the base registry holds none.""" + from pcapkit.const.reg.apptype import AppType, TransportProtocol + from pcapkit.utilities.exceptions import EnumValueError + + for registry in _registries(): + member = next(iter(registry)) # type: ignore[call-overload] + for default in ('x', member.port, member): + with self.subTest(registry=registry.__name__, default=default): + with self.assertRaises(EnumValueError) as caught: + registry.get(99999, default) # type: ignore[attr-defined] + self.assertEqual(str(caught.exception), + f'99999 is not a valid {registry.__name__}') + with self.subTest(registry=registry.__name__, proto=None): + with self.assertRaises(EnumValueError) as caught: + AppType.get(99999, member.value) + self.assertEqual(str(caught.exception), + f'{TransportProtocol.undefined!r} names no transport protocol ' + 'registry of AppType') + + +class AppTypeGetMiss2545Tests(unittest.TestCase): + """With no usable ``default``, every miss is a quiet ``EnumValueError``.""" + + def assertQuietValueMiss(self, message: 'str', registry: 'type', key: 'object', + **kwargs: 'object') -> None: + """Assert ``registry.get(key, **kwargs)`` raises a quiet ``EnumValueError`` + carrying ``message``, which is still a :exc:`ValueError`.""" + from pcapkit.utilities.exceptions import EnumValueError + from pcapkit.utilities.logging import logger + from tests.utilities._harness import capture + + with capture(logger) as recorder: + with self.assertRaises(ValueError) as caught: + registry.get(key, **kwargs) # type: ignore[attr-defined] + self.assertIs(type(caught.exception), EnumValueError) + self.assertEqual(str(caught.exception), message) + self.assertEqual(recorder.messages, []) + + def test_a_port_out_of_range(self) -> None: + for registry in _registries(): + for key in OUT_OF_RANGE: + with self.subTest(registry=registry.__name__, key=key): + self.assertQuietValueMiss(f'{key!r} is not a valid {registry.__name__}', + registry, key) + + def test_a_key_that_is_not_a_port(self) -> None: + for registry in _registries(): + for key in NOT_A_PORT: + with self.subTest(registry=registry.__name__, key=key): + self.assertQuietValueMiss( + f'{key!r} is not a valid port number for {registry.__name__}', + registry, key) + + def test_a_proto_naming_no_registry(self) -> None: + from pcapkit.const.reg.apptype import AppType, TransportProtocol + + for proto in (TransportProtocol.undefined, 17): + with self.subTest(proto=proto): + self.assertQuietValueMiss( + f'{proto!r} names no transport protocol registry of AppType', + AppType, 80, proto=proto) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/vendor/test_vendor_apptype_get_template_agreement_2048_unit.py b/tests/vendor/test_vendor_apptype_get_template_agreement_2048_unit.py index 28a3ed9ad0..f536e2a268 100644 --- a/tests/vendor/test_vendor_apptype_get_template_agreement_2048_unit.py +++ b/tests/vendor/test_vendor_apptype_get_template_agreement_2048_unit.py @@ -31,7 +31,7 @@ HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS) #: Where ``get`` starts and where the method after it starts, in both texts. -GET_START = " @classmethod\n def get(cls, key: 'int', *," +GET_START = " @classmethod\n def get(cls, key: 'int', default: 'Any' = NO_DEFAULT, *," GET_END = ' @classmethod\n def get_all('