diff --git a/docs/source/contributing/conventions.rst b/docs/source/contributing/conventions.rst index 9bccf2f67..d7d7968de 100644 --- a/docs/source/contributing/conventions.rst +++ b/docs/source/contributing/conventions.rst @@ -385,22 +385,25 @@ Three things about it are easy to get wrong: .. note:: - Re-parenting the remaining helper enumerations onto - :class:`~pcapkit.corekit.enum.EnumLookup` is **phase 2** of - `#877 `__, and it is **partly - done** rather than pending: the phase landed for 17 of the 24 non-registry - enumerations. **Seven are still outside the hierarchy**, measured by a runtime - walk over both the :mod:`enum` and :mod:`aenum` flavours: ``CommandType`` and - ``ConformanceRequirement`` in :mod:`pcapkit.const.ftp.command`, ``ESPStatus`` in - :mod:`pcapkit.protocols.internet.esp`, and all four - :mod:`pcapkit.protocols.internet.mh` helpers - (``FastBindingAcknowledgmentStatus``, ``IPv6AddressPrefixCode``, - ``LMAAddressCode``, ``LocalizedRoutingStatus``). Each sat in a file another pull - request held open while phase 2 ran, which is the whole reason the phase was split - in two: phase 1 is behaviour-preserving on its own, so it could land while work - was still in flight on the files a re-parent touches. Do not read the seven as a - ruling against re-parenting them -- they are the remainder of a phase, not an - exception to it. + Re-parenting every non-registry enumeration onto + :class:`~pcapkit.corekit.enum.EnumLookup` was **phase 2** of + `#877 `__, and it is now + **complete**: the phase landed for 24 of the 24 non-registry enumerations. + **Zero enumerations remain outside the hierarchy**, measured by the same runtime + walk over both the :mod:`enum` and :mod:`aenum` flavours that once found seven. + + It landed in two pull requests rather than one. Seven of the 24 sat in files + other pull requests were editing around the same time: ``CommandType`` and + ``ConformanceRequirement`` in :mod:`pcapkit.const.ftp.command` and its vendor + template, both touched by `#913 `__; + and ``ESPStatus`` in :mod:`pcapkit.protocols.internet.esp` plus all four + :mod:`pcapkit.protocols.internet.mh` helpers (``FastBindingAcknowledgmentStatus``, + ``IPv6AddressPrefixCode``, ``LMAAddressCode``, ``LocalizedRoutingStatus``), both + files touched by `#924 `__. The + first pass (`#921 `__) is + behaviour-preserving on its own, so the other 17 could land without waiting on + those files; the remaining seven followed once both had merged + (`#930 `__). What a Failed Lookup Raises ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -676,11 +679,13 @@ wider than a case fix: specification's own tokens -- the measured spelling disagreement above would make these two case-insensitive. Nothing looks them up by string today, though: the crawler translates the CSV's lower-case letters to the upper-case member names at - generation time, and neither class inherits - :class:`~pcapkit.corekit.enum.EnumLookup` yet -- both are in the seven phase 2 has - not reached, per the note above. Re-parenting them is phase 2 of - `#877 `__, which is where the - question belongs. + generation time. Both classes now inherit + :class:`~pcapkit.corekit.enum.EnumLookup` -- + `#930 `__ finished re-parenting + them, per the note above -- so a ``get`` exists on each, case-sensitive like the + base's own. Whether to fold case to match ``TransportProtocol``'s own override is a + design question for whoever writes the first string-keyed caller, not one this + audit settles. One case fold also lives **outside** any ``get``, and so escapes this convention entirely: ``_resolve`` in :mod:`pcapkit.protocols.internet.esp` upper-cases its diff --git a/pcapkit/const/ftp/command.py b/pcapkit/const/ftp/command.py index 708f8f308..862891a2e 100644 --- a/pcapkit/const/ftp/command.py +++ b/pcapkit/const/ftp/command.py @@ -15,7 +15,7 @@ from aenum import IntEnum, IntFlag, StrEnum, auto -from pcapkit.corekit.enum import NO_DEFAULT, EnumRegistry +from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup, EnumRegistry if TYPE_CHECKING: from typing import Any, Optional, Type @@ -183,8 +183,17 @@ def _missing_(cls, value: 'str') -> 'FEATCode': return cls._unregistered_member(value, value.upper()) -class CommandType(IntFlag): - """Type of "kind" of command, based on :rfc:`959#section-4.1`.""" +class CommandType(EnumLookup, IntFlag): + """Type of "kind" of command, based on :rfc:`959#section-4.1`. + + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2. Pure re-parenting as far as + ``get``/``get_all`` are concerned -- this class defines no ``get`` of its + own to reconcile with the base -- and its own :meth:`_missing_` range + guard below is untouched, since :class:`EnumLookup` does not touch that + hook. + + """ undefined = 0 @@ -208,8 +217,15 @@ def _missing_(cls, value: 'int') -> 'CommandType': return super()._missing_(value) -class ConformanceRequirement(IntEnum): - """Expectation for support in modern FTP implementations.""" +class ConformanceRequirement(EnumLookup, IntEnum): + """Expectation for support in modern FTP implementations. + + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2 -- pure re-parenting, since this + class defines neither ``get`` nor ``_missing_`` of its own to reconcile + with the base. + + """ #: Mandatory to implement. M = auto() diff --git a/pcapkit/corekit/enum.py b/pcapkit/corekit/enum.py index aa7d6db21..7f4628ccb 100644 --- a/pcapkit/corekit/enum.py +++ b/pcapkit/corekit/enum.py @@ -42,10 +42,14 @@ .. note:: - Re-parenting the remaining helper enumerations onto :class:`EnumLookup` is - **phase 2** of GitHub issue #877 and has not happened yet: introducing the - base is deliberately behaviour-preserving on its own, so that it could land - while other work was still in flight on the files the re-parent touches. + Re-parenting every non-registry enumeration onto :class:`EnumLookup` was + **phase 2** of GitHub issue #877, and it is now **complete**: introducing + the base above was deliberately behaviour-preserving on its own, so that it + could land while other work was still in flight on the files the re-parent + touches, and the phase itself landed in two pull requests for exactly that + reason -- #921 for the 17 enumerations that were free to move at once, and + `#930 `__ for the + remaining seven once the files holding them freed up. The registry tier's own shape is the earlier ruling on GitHub issue #842, verbatim: *"to finalise the abstraction idea, get/get_all/register/register_alias diff --git a/pcapkit/protocols/internet/esp.py b/pcapkit/protocols/internet/esp.py index 9572633d1..88cfcd591 100644 --- a/pcapkit/protocols/internet/esp.py +++ b/pcapkit/protocols/internet/esp.py @@ -173,6 +173,7 @@ from pcapkit.const.esp.integrity import Integrity from pcapkit.const.reg.transtype import TransType as Enum_TransType from pcapkit.corekit.context import ProtocolContext +from pcapkit.corekit.enum import EnumLookup from pcapkit.corekit.infoclass import Info, info_final from pcapkit.protocols.data.internet.esp import ESP as Data_ESP from pcapkit.protocols.internet.ipsec import IPsec @@ -454,8 +455,15 @@ def get(cls, value: 'Integrity | str | int') -> 'IntegritySuite': ############################################################################## -class ESPStatus(enum.IntEnum): - """Outcome of ESP payload processing.""" +class ESPStatus(EnumLookup, enum.IntEnum): + """Outcome of ESP payload processing. + + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2 -- pure re-parenting, since this + class defines neither ``get`` nor ``_missing_`` of its own to reconcile + with the base. + + """ #: The payload was decrypted and its trailer recovered. DECRYPTED = 0 diff --git a/pcapkit/protocols/internet/mh.py b/pcapkit/protocols/internet/mh.py index ba674b73a..1f2f8aa1d 100644 --- a/pcapkit/protocols/internet/mh.py +++ b/pcapkit/protocols/internet/mh.py @@ -77,6 +77,7 @@ UpdateNotificationACKStatus as Enum_UpdateNotificationACKStatus from pcapkit.const.mh.upn_reason import UpdateNotificationReason as Enum_UpdateNotificationReason from pcapkit.const.reg.transtype import TransType as Enum_TransType +from pcapkit.corekit.enum import EnumLookup from pcapkit.corekit.fields.ipaddress import parse_ip_address from pcapkit.corekit.multidict import OrderedMultiDict from pcapkit.protocols.data.internet.mh import MH as Data_MH @@ -565,7 +566,7 @@ class PMIPv6Timestamp(collections.namedtuple('PMIPv6Timestamp', 'seconds fractio fraction: int -class FastBindingAcknowledgmentStatus(IntEnum): +class FastBindingAcknowledgmentStatus(EnumLookup, IntEnum): """[FastBindingAcknowledgmentStatus] Fast Binding Acknowledgment Status Codes. Status values of the fast binding acknowledgment (FBack) message, c.f., @@ -574,6 +575,23 @@ class FastBindingAcknowledgmentStatus(IntEnum): above that it was rejected. Note: + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2. :meth:`get` and + :meth:`_missing_` below are untouched -- #923 already converted + :meth:`get`'s own name-miss to the in-library + :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, which is exactly + the shape the base's own ``get`` uses, so there is nothing to + reconcile. Unlike :meth:`~pcapkit.const.reg.apptype.apptype. + TransportProtocol.get` and :meth:`~pcapkit.protocols.application. + ngap.Criticality.get` in GitHub issue #921, :meth:`get` keeps its + ``@staticmethod`` decorator rather than becoming a delegating + ``classmethod`` -- it never calls ``super().get(...)``, so the + :exc:`RuntimeError` trap a ``staticmethod`` delegating to a + ``classmethod`` base would hit does not apply here, and the + resulting ``mypy`` ``[override]`` complaint about the signature + mismatch (no ``cls``, no ``default``) is silenced rather than + resolved by widening the signature. + :rfc:`5568#section-6.2.3` defines these values inline and IANA keeps no registry of them, so the enumeration lives here rather than in :mod:`pcapkit.const.mh`. It is also **not** interchangeable with the @@ -624,9 +642,27 @@ class FastBindingAcknowledgmentStatus(IntEnum): Incorrect_interface_identifier_length = 131 @staticmethod - def get(key: 'int | str') -> 'FastBindingAcknowledgmentStatus': + def get( # type: ignore[override] # pylint: disable=arguments-differ + key: 'int | str') -> 'FastBindingAcknowledgmentStatus': """Backport support for original codes. + Raises quietly on a name miss, matching the base's own + :meth:`~pcapkit.corekit.enum.EnumLookup.get` + (:mod:`pcapkit.corekit.enum`, the ``EnumKeyError`` raised at its + ``str`` branch) rather than diverging from it. This override used + to raise loud instead -- logging once at :data:`logging.CRITICAL` + and setting :data:`sys.tracebacklimit` to ``0`` process-wide -- + until GitHub issue #930 converged it onto house convention, + settled on GitHub issue #933's follow-up ruling, verbatim: *"Oh + wait. I meant, they should follow house convention and not to be + loud."* Re-parenting onto + :class:`~pcapkit.corekit.enum.EnumLookup` is what makes the + convergence reach further than this one method: :meth:`get_all + ` did not exist on this + class before #930 and is now inherited from the base, which calls + this ``get`` internally -- so a name miss reached through + ``get_all`` is quiet too, for the same reason. + Args: key: Key to get enum item. @@ -657,7 +693,8 @@ def get(key: 'int | str') -> 'FastBindingAcknowledgmentStatus': return FastBindingAcknowledgmentStatus[key] # type: ignore[misc] except KeyError: raise EnumKeyError('%r is not a valid %s' % - (key, FastBindingAcknowledgmentStatus.__name__)) from None + (key, FastBindingAcknowledgmentStatus.__name__), + quiet=True) from None @classmethod def _missing_(cls, value: 'int') -> 'NoReturn': @@ -676,7 +713,7 @@ def _missing_(cls, value: 'int') -> 'NoReturn': raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) -class IPv6AddressPrefixCode(IntEnum): +class IPv6AddressPrefixCode(EnumLookup, IntEnum): """[IPv6AddressPrefixCode] Mobility Header IPv6 Address/Prefix Option Codes. Option codes of the mobility header IPv6 address/prefix option, which @@ -684,6 +721,23 @@ class IPv6AddressPrefixCode(IntEnum): :rfc:`5568#section-6.4.2`. Note: + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2. :meth:`get` and + :meth:`_missing_` below are untouched -- #923 already converted + :meth:`get`'s own name-miss to the in-library + :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, which is exactly + the shape the base's own ``get`` uses, so there is nothing to + reconcile. Unlike :meth:`~pcapkit.const.reg.apptype.apptype. + TransportProtocol.get` and :meth:`~pcapkit.protocols.application. + ngap.Criticality.get` in GitHub issue #921, :meth:`get` keeps its + ``@staticmethod`` decorator rather than becoming a delegating + ``classmethod`` -- it never calls ``super().get(...)``, so the + :exc:`RuntimeError` trap a ``staticmethod`` delegating to a + ``classmethod`` base would hit does not apply here, and the + resulting ``mypy`` ``[override]`` complaint about the signature + mismatch (no ``cls``, no ``default``) is silenced rather than + resolved by widening the signature. + :rfc:`5568#section-6.4.2` defines these values inline and IANA keeps no registry of them, so the enumeration lives here rather than in :mod:`pcapkit.const.mh`. The identical code space of the neighbor @@ -725,9 +779,27 @@ class IPv6AddressPrefixCode(IntEnum): NAR_Prefix = 4 @staticmethod - def get(key: 'int | str') -> 'IPv6AddressPrefixCode': + def get( # type: ignore[override] # pylint: disable=arguments-differ + key: 'int | str') -> 'IPv6AddressPrefixCode': """Backport support for original codes. + Raises quietly on a name miss, matching the base's own + :meth:`~pcapkit.corekit.enum.EnumLookup.get` + (:mod:`pcapkit.corekit.enum`, the ``EnumKeyError`` raised at its + ``str`` branch) rather than diverging from it. This override used + to raise loud instead -- logging once at :data:`logging.CRITICAL` + and setting :data:`sys.tracebacklimit` to ``0`` process-wide -- + until GitHub issue #930 converged it onto house convention, + settled on GitHub issue #933's follow-up ruling, verbatim: *"Oh + wait. I meant, they should follow house convention and not to be + loud."* Re-parenting onto + :class:`~pcapkit.corekit.enum.EnumLookup` is what makes the + convergence reach further than this one method: :meth:`get_all + ` did not exist on this + class before #930 and is now inherited from the base, which calls + this ``get`` internally -- so a name miss reached through + ``get_all`` is quiet too, for the same reason. + Args: key: Key to get enum item. @@ -758,7 +830,8 @@ def get(key: 'int | str') -> 'IPv6AddressPrefixCode': return IPv6AddressPrefixCode[key] # type: ignore[misc] except KeyError: raise EnumKeyError('%r is not a valid %s' % - (key, IPv6AddressPrefixCode.__name__)) from None + (key, IPv6AddressPrefixCode.__name__), + quiet=True) from None @classmethod def _missing_(cls, value: 'int') -> 'NoReturn': @@ -777,7 +850,7 @@ def _missing_(cls, value: 'int') -> 'NoReturn': raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) -class LocalizedRoutingStatus(IntEnum): +class LocalizedRoutingStatus(EnumLookup, IntEnum): """[LocalizedRoutingStatus] Localized Routing Acknowledgment Status Codes. Status values of the localized routing acknowledgment (LRA) message, c.f., @@ -785,6 +858,13 @@ class LocalizedRoutingStatus(IntEnum): was processed successfully, values of ``128`` and above that it was rejected. Note: + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2 -- pure re-parenting as far as + ``get``/``get_all`` are concerned, since this class defines no + ``get`` of its own to reconcile with the base; its own + :meth:`_missing_` below is untouched, since :class:`EnumLookup` does + not touch that hook. + :rfc:`6705#section-10.2` defines these values inline and IANA keeps no registry of them -- neither a dedicated one nor entries in the general *Status Codes* registry -- so the enumeration lives here rather than in @@ -811,11 +891,15 @@ class LocalizedRoutingStatus(IntEnum): naming merely an unassigned byte is simply a more likely way to reach it. See GitHub issue #880. - There is no ``get()`` backport here, unlike + There is no hand-rolled ``get()`` backport here, unlike :class:`FastBindingAcknowledgmentStatus` and :class:`IPv6AddressPrefixCode`: it had zero callers repo-wide -- tests included -- so GitHub issue #880 deleted it outright rather than - rebuilding it on the immutable contract. + rebuilding it on the immutable contract. GitHub issue #930's + re-parenting above gives this class ``get``/``get_all`` again, but as + the base's own bare lookup rather than a bespoke override -- it still + cannot mint, so an unassigned value raises through ``get`` exactly as + it does through the bare constructor. """ @@ -845,13 +929,20 @@ def _missing_(cls, value: 'int') -> 'NoReturn': raise EnumValueError('%r is not a valid %s' % (value, cls.__name__)) -class LMAAddressCode(IntEnum): +class LMAAddressCode(EnumLookup, IntEnum): """[LMAAddressCode] Local Mobility Anchor Address Option Codes. Option codes of the local mobility anchor address option, which say which address family the option carries, c.f., :rfc:`5949#section-6.2.2`. Note: + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2 -- pure re-parenting as far as + ``get``/``get_all`` are concerned, since this class defines no + ``get`` of its own to reconcile with the base; its own + :meth:`_missing_` below is untouched, since :class:`EnumLookup` does + not touch that hook. + :rfc:`5949#section-6.2.2` defines these values inline and IANA keeps no registry of them, so the enumeration lives here rather than in :mod:`pcapkit.const.mh`. @@ -875,11 +966,15 @@ class LMAAddressCode(IntEnum): naming merely an unassigned byte is simply a more likely way to reach it. See GitHub issue #880. - There is no ``get()`` backport here, unlike + There is no hand-rolled ``get()`` backport here, unlike :class:`FastBindingAcknowledgmentStatus` and :class:`IPv6AddressPrefixCode`: it had zero callers repo-wide -- tests included -- so GitHub issue #880 deleted it outright rather than - rebuilding it on the immutable contract. + rebuilding it on the immutable contract. GitHub issue #930's + re-parenting above gives this class ``get``/``get_all`` again, but as + the base's own bare lookup rather than a bespoke override -- it still + cannot mint, so an unassigned value raises through ``get`` exactly as + it does through the bare constructor. """ diff --git a/pcapkit/vendor/ftp/command.py b/pcapkit/vendor/ftp/command.py index 9f662965a..3780c74ae 100644 --- a/pcapkit/vendor/ftp/command.py +++ b/pcapkit/vendor/ftp/command.py @@ -55,7 +55,7 @@ from aenum import IntEnum, IntFlag, StrEnum, auto -from pcapkit.corekit.enum import NO_DEFAULT, EnumRegistry +from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup, EnumRegistry if TYPE_CHECKING: from typing import Any, Optional, Type @@ -195,8 +195,17 @@ def _missing_(cls, value: 'str') -> 'FEATCode': return cls._unregistered_member(value, value.upper()) -class CommandType(IntFlag): - """Type of "kind" of command, based on :rfc:`959#section-4.1`.""" +class CommandType(EnumLookup, IntFlag): + """Type of "kind" of command, based on :rfc:`959#section-4.1`. + + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2. Pure re-parenting as far as + ``get``/``get_all`` are concerned -- this class defines no ``get`` of its + own to reconcile with the base -- and its own :meth:`_missing_` range + guard below is untouched, since :class:`EnumLookup` does not touch that + hook. + + """ undefined = 0 @@ -220,8 +229,15 @@ def _missing_(cls, value: 'int') -> 'CommandType': return super()._missing_(value) -class ConformanceRequirement(IntEnum): - """Expectation for support in modern FTP implementations.""" +class ConformanceRequirement(EnumLookup, IntEnum): + """Expectation for support in modern FTP implementations. + + Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub + issue #930, finishing #877's phase 2 -- pure re-parenting, since this + class defines neither ``get`` nor ``_missing_`` of its own to reconcile + with the base. + + """ #: Mandatory to implement. M = auto() diff --git a/tests/const/test_const_enum_get.py b/tests/const/test_const_enum_get.py index 75d79426f..d3f849781 100644 --- a/tests/const/test_const_enum_get.py +++ b/tests/const/test_const_enum_get.py @@ -109,27 +109,23 @@ }) #: Enums carrying no ``get(key, default)``, so there is no ``default`` to drop. -#: These two are helper enums describing a registry's columns rather than -#: registries themselves and have no ``get`` at all -- held here rather than -#: moved by GitHub issue #877, since :mod:`pcapkit.const.ftp.command` is held -#: by #913 pending its merge. +#: Empty since GitHub issue #930: its two occupants, +#: :class:`~pcapkit.const.ftp.command.CommandType` and +#: :class:`~pcapkit.const.ftp.command.ConformanceRequirement`, were the last +#: of #877's non-registry enumerations still outside +#: :class:`~pcapkit.corekit.enum.EnumLookup` -- held here rather than moved by +#: GitHub issue #877's own PR because :mod:`pcapkit.const.ftp.command` was +#: still held by #913 at the time. Re-parenting them gave each the base +#: ``get(key, default)`` for the first time, so both moved into the main +#: sweep below instead, the same way #877's own re-parenting already moved +#: :class:`~pcapkit.const.reg.apptype.TransportProtocol` out of this set. #: #: :class:`~pcapkit.const.ftp.return_code.GroupingInformation` and #: :class:`~pcapkit.const.ftp.return_code.ResponseKind` were here too until #: GitHub issue #860 step 2 brought them onto #: :class:`~pcapkit.corekit.enum.EnumRegistry` -- they inherit the base #: ``get(key, default)`` now, so they moved into the main sweep below instead. -#: :class:`~pcapkit.const.reg.apptype.TransportProtocol` was here too until -#: GitHub issue #877's re-parenting onto -#: :class:`~pcapkit.corekit.enum.EnumLookup` gave its own ``get`` override a -#: ``default`` parameter for the first time -- forwarded verbatim to the -#: base, purely to keep the override's signature a valid ``classmethod`` -#: override of one that already had it -- so it moved into the main sweep -#: below as well. -EXPECTED_WITHOUT_AN_INTEGER_DEFAULT = frozenset({ - 'pcapkit.const.ftp.command.CommandType', - 'pcapkit.const.ftp.command.ConformanceRequirement', -}) +EXPECTED_WITHOUT_AN_INTEGER_DEFAULT = frozenset() # type: frozenset[str] #: :class:`~pcapkit.const.pcapng.filter_type.FilterType` declares *no* static #: members at all -- every one of its 256 codes reaches ``_missing_``. Before @@ -370,8 +366,12 @@ def test_every_integer_path_consults_the_default(self) -> None: # pcapkit.const at all; plus 1 for GitHub issue #877's re-parenting of # ``TransportProtocol`` onto ``EnumLookup``, which gave its own ``get`` # override a forwarding ``default`` parameter and moved it out of - # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` the same way. - self.assertEqual(covered, 115) + # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` the same way; plus 2 for + # GitHub issue #930's re-parenting of ``CommandType`` and + # ``ConformanceRequirement`` onto ``EnumLookup``, which gave each the + # base ``get(key, default)`` for the first time and emptied + # ``EXPECTED_WITHOUT_AN_INTEGER_DEFAULT`` entirely. + self.assertEqual(covered, 117) def test_the_always_resolving_registries_have_nothing_to_fall_back_to(self) -> None: """The two registries excused from the sweep, and why. diff --git a/tests/const/test_const_ftp_featcode_case_903_unit.py b/tests/const/test_const_ftp_featcode_case_903_unit.py index 297be0004..9e09be09b 100644 --- a/tests/const/test_const_ftp_featcode_case_903_unit.py +++ b/tests/const/test_const_ftp_featcode_case_903_unit.py @@ -312,16 +312,21 @@ def test_the_crawler_template_carries_the_same_get(self) -> 'None': def test_the_generated_module_imports_what_the_override_needs(self) -> 'None': """``NO_DEFAULT`` is the sentinel the override's own signature carries, - so the template has to import it as well as emit the method.""" + so the template has to import it as well as emit the method. + + ``EnumLookup`` joined the same import with GitHub issue #930, which + re-parented ``CommandType`` and ``ConformanceRequirement`` -- the last + two of #877's non-registry enumerations -- onto it. + """ from pcapkit.vendor.ftp.command import LINE rendered = LINE('Command', 'FTP Command', '', '', 'pcapkit.vendor.ftp.command') - self.assertIn('from pcapkit.corekit.enum import NO_DEFAULT, EnumRegistry', + self.assertIn('from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup, EnumRegistry', rendered) import pcapkit.const.ftp.command as generated - self.assertIn('from pcapkit.corekit.enum import NO_DEFAULT, EnumRegistry', + self.assertIn('from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup, EnumRegistry', inspect.getsource(generated)) diff --git a/tests/corekit/test_enum_lookup_reparent_930_unit.py b/tests/corekit/test_enum_lookup_reparent_930_unit.py new file mode 100644 index 000000000..30bec0572 --- /dev/null +++ b/tests/corekit/test_enum_lookup_reparent_930_unit.py @@ -0,0 +1,602 @@ +# -*- coding: utf-8 -*- +"""GitHub issue #930, the blocked half of #877's phase 2: re-parenting the last +seven non-registry enumerations onto :class:`~pcapkit.corekit.enum.EnumLookup`. + +GitHub issue #921 re-parented 17 of the 24 non-registry enumerations and deliberately +left seven alone, because the files holding them were still open under other pull +requests at the time: :mod:`pcapkit.const.ftp.command` (``CommandType``, +``ConformanceRequirement``) under #913, and :mod:`pcapkit.protocols.internet.esp` +(``ESPStatus``) plus :mod:`pcapkit.protocols.internet.mh` +(``FastBindingAcknowledgmentStatus``, ``IPv6AddressPrefixCode``, ``LMAAddressCode``, +``LocalizedRoutingStatus``) under #904. Both blockers have since merged, and this +module pins that the remainder actually took the base -- finishing phase 2 at 24 of 24 +non-registry enumerations, with zero still outside the hierarchy. + +Two of the seven, :class:`FastBindingAcknowledgmentStatus` and +:class:`IPv6AddressPrefixCode`, already defined their own ``get`` -- both as a +:class:`staticmethod`, the exact shape GitHub issue #908 found dangerous once a base +``get`` becomes a :class:`classmethod`: calling ``super().get(...)`` from a +``staticmethod`` raises :exc:`RuntimeError: super(): no arguments`. Neither override +calls ``super()`` at all, so the trap does not fire, and per this issue's own brief they +are left **untouched** -- their ``except KeyError: raise EnumKeyError(...)`` conversions +were put there by GitHub issue #923 and already answer a name miss in exactly the shape +the base now uses. Keeping the plain ``@staticmethod`` does cost something, though: +``mypy``'s ``[override]`` check and ``pylint``'s ``arguments-differ`` both flag the +resulting shape mismatch against the base's ``classmethod`` (``cls, key, default``) +signature, and both are silenced rather than resolved by widening the signature -- +:class:`ReparentedBasesTests` pins, alongside each class's own base-tuple change, +that the decorator itself survived the re-parenting, which is what makes those +suppressions still apply to the right thing. + +Re-parenting also makes both kept overrides reachable through a door that +did not exist before: :meth:`~pcapkit.corekit.enum.EnumLookup.get_all`, +inherited from the base for the first time, calls ``get`` internally. Before +this issue, that mattered because ``get`` itself raised **loud**: both +overrides used to log once at :data:`logging.CRITICAL` and set +:data:`sys.tracebacklimit` to ``0`` process-wide on a name miss, unlike the +base's own quiet raise. GitHub issue #930 converges both onto the base's +quiet shape instead -- a real behaviour change, not merely a re-parent -- +settled on GitHub issue #933's follow-up ruling, verbatim: *"Oh wait. I +meant, they should follow house convention and not to be loud."* +:class:`KeptOverrideQuietnessTests` pins both halves of that: the ``get`` +half, which genuinely changes (loud on the tree before this issue, quiet +here), and the ``get_all`` half, which is new outright (the attribute does +not exist on that tree at all). + +The other five -- :class:`CommandType`, :class:`ConformanceRequirement`, +:class:`ESPStatus`, :class:`LocalizedRoutingStatus` and :class:`LMAAddressCode` -- are +pure re-parents: none defines a ``get`` of its own to reconcile with the base, so each +gains ``get``/``get_all`` for the first time. :class:`CommandType`, +:class:`LocalizedRoutingStatus` and :class:`LMAAddressCode` do carry their own +``_missing_`` range guards, which are untouched -- :class:`EnumLookup` does not +override that hook, so a re-parent cannot change what it does. + +On the tree before this change, every ``.get(...)`` call below fails with +``AttributeError: type object '' has no attribute 'get'`` for the five pure +re-parents, and every base-tuple assertion in :class:`ReparentedBasesTests` fails since +:class:`~pcapkit.corekit.enum.EnumLookup` is not yet in any of these seven classes' +``__bases__`` or MRO. Quoted verbatim from an actual run of +:meth:`ReparentedBasesTests.test_command_type` with the three source files reverted to +``af2324522`` (origin/main immediately before this change; structurally identical to +``4f3d43df7`` for these seven classes) while this module itself stayed as written:: + + AssertionError: Tuples differ: (,) != (, ) + + First differing element 0: + + + + Second tuple contains 1 additional elements. + First extra element 1: + + + - (,) + + (, ) + +Measured against that reverted tree (plain :mod:`unittest`, 27 test methods, +each ``subTest`` resolved back to its parent method via +``getattr(test, 'test_case', test)`` rather than counted on its own -- +without that resolution a ``subTest``-only failure is recorded against +``unittest.case._SubTest`` and its parent method misreads as passing): +**22 of the 27 fail or error, and five hold** -- + +* ``ZeroRemainOutsideEnumLookupTests.test_the_walk_found_something_to_count`` -- + a guard that keeps the census assertion from passing vacuously on an empty + discovery, not itself a claim about this change, so it is expected to hold + either way. +* ``FailedLookupTests.test_fast_binding_acknowledgment_status``, + ``FailedLookupTests.test_ipv6_address_prefix_code``, + ``GetByNameAndValueTests.test_fast_binding_acknowledgment_status`` and + ``GetByNameAndValueTests.test_ipv6_address_prefix_code`` hold for the most + interesting reason in this file: :class:`FastBindingAcknowledgmentStatus` + and :class:`IPv6AddressPrefixCode` already had a working ``get`` before this + change -- their own kept :class:`staticmethod`, described above -- so their + lookup and failed-lookup behaviour is unaffected by the re-parent and these + four assertions hold on both trees. What *does* fail for both classes on the + reverted tree is their own :class:`ReparentedBasesTests` method, since the + base-tuple/MRO change is real for all seven regardless of whether ``get`` + itself was already working. + +Neither method of :class:`KeptOverrideQuietnessTests` holds, and deliberately +so -- both are pinning the one thing this issue actually changes about the two +kept overrides. ``test_get_is_now_quiet_on_both_classes`` fails against the +reverted tree because ``get`` really was loud there (see that class's own +docstring): this is not a scaffolding failure, it is the behaviour change +itself, caught in the act. ``test_get_all_is_new_and_quiet_too`` fails with +``AttributeError`` instead, since ``get_all`` does not exist on the reverted +tree at all. + +""" +from __future__ import annotations + +import importlib +import inspect +import pkgutil +import sys +import unittest + +from pcapkit.corekit.enum import EnumLookup +from pcapkit.utilities.exceptions import EnumKeyError, EnumValueError +from pcapkit.utilities.logging import logger +from tests.utilities._harness import capture + +__all__ = [ + 'ReparentedBasesTests', 'GetByNameAndValueTests', 'FailedLookupTests', + 'KeptOverrideQuietnessTests', 'NoMintingTests', 'PureReparentClassmethodTests', + 'ZeroRemainOutsideEnumLookupTests', +] + + +class ReparentedBasesTests(unittest.TestCase): + """Every one of the seven gained :class:`EnumLookup` as a base, mixed in + *ahead of* its enum base so ``_member_type_`` still resolves to + :class:`int`, and none grew a member in the process -- this issue's own + brief is explicit that a re-parent minting one silently is exactly the + defect this programme exists to prevent, so each test below pins both in + one place rather than trusting that a passing base-tuple assertion says + anything about the member table. ``__members__`` (which counts aliases) + and ``list(cls)`` (which does not) are pinned separately, since the two + diverge on :class:`CommandType`: its ``undefined = 0`` member is a real, + named member -- not an alias -- but :class:`~aenum.Flag` iteration omits + the zero value by convention, the same way it always has. + """ + + def test_command_type(self) -> None: + from aenum import IntFlag + + from pcapkit.const.ftp.command import CommandType + + self.assertEqual(CommandType.__bases__, (EnumLookup, IntFlag)) + self.assertIn(EnumLookup, CommandType.__mro__) + self.assertEqual(len(CommandType.__members__), 4) + self.assertEqual(len(list(CommandType)), 3) + + def test_conformance_requirement(self) -> None: + from aenum import IntEnum + + from pcapkit.const.ftp.command import ConformanceRequirement + + self.assertEqual(ConformanceRequirement.__bases__, (EnumLookup, IntEnum)) + self.assertIn(EnumLookup, ConformanceRequirement.__mro__) + self.assertEqual(len(ConformanceRequirement.__members__), 3) + self.assertEqual(len(list(ConformanceRequirement)), 3) + + def test_esp_status(self) -> None: + import enum + + from pcapkit.protocols.internet.esp import ESPStatus + + self.assertEqual(ESPStatus.__bases__, (EnumLookup, enum.IntEnum)) + self.assertIn(EnumLookup, ESPStatus.__mro__) + self.assertEqual(len(ESPStatus.__members__), 6) + self.assertEqual(len(list(ESPStatus)), 6) + + def test_fast_binding_acknowledgment_status(self) -> None: + """Also pins that its kept ``get`` override is still a + :class:`staticmethod` -- unlike ``TransportProtocol.get`` and + ``Criticality.get`` in GitHub issue #921, it never calls + ``super().get(...)``, so there is no delegation to convert it for.""" + from aenum import IntEnum + + from pcapkit.protocols.internet.mh import FastBindingAcknowledgmentStatus + + self.assertEqual(FastBindingAcknowledgmentStatus.__bases__, (EnumLookup, IntEnum)) + self.assertIn(EnumLookup, FastBindingAcknowledgmentStatus.__mro__) + self.assertEqual(len(FastBindingAcknowledgmentStatus.__members__), 6) + self.assertEqual(len(list(FastBindingAcknowledgmentStatus)), 6) + self.assertIsInstance( + inspect.getattr_static(FastBindingAcknowledgmentStatus, 'get'), staticmethod) + + def test_ipv6_address_prefix_code(self) -> None: + """Also pins that its kept ``get`` override is still a + :class:`staticmethod`, for the same reason as + :class:`FastBindingAcknowledgmentStatus`.""" + from aenum import IntEnum + + from pcapkit.protocols.internet.mh import IPv6AddressPrefixCode + + self.assertEqual(IPv6AddressPrefixCode.__bases__, (EnumLookup, IntEnum)) + self.assertIn(EnumLookup, IPv6AddressPrefixCode.__mro__) + self.assertEqual(len(IPv6AddressPrefixCode.__members__), 4) + self.assertEqual(len(list(IPv6AddressPrefixCode)), 4) + self.assertIsInstance( + inspect.getattr_static(IPv6AddressPrefixCode, 'get'), staticmethod) + + def test_localized_routing_status(self) -> None: + from aenum import IntEnum + + from pcapkit.protocols.internet.mh import LocalizedRoutingStatus + + self.assertEqual(LocalizedRoutingStatus.__bases__, (EnumLookup, IntEnum)) + self.assertIn(EnumLookup, LocalizedRoutingStatus.__mro__) + self.assertEqual(len(LocalizedRoutingStatus.__members__), 3) + self.assertEqual(len(list(LocalizedRoutingStatus)), 3) + + def test_lma_address_code(self) -> None: + from aenum import IntEnum + + from pcapkit.protocols.internet.mh import LMAAddressCode + + self.assertEqual(LMAAddressCode.__bases__, (EnumLookup, IntEnum)) + self.assertIn(EnumLookup, LMAAddressCode.__mro__) + self.assertEqual(len(LMAAddressCode.__members__), 3) + self.assertEqual(len(list(LMAAddressCode)), 3) + + +class NoMintingTests(unittest.TestCase): + """None of the seven mints on a miss, exercised through ``get`` itself + rather than only through the static counts above.""" + + def test_a_battery_of_lookups_does_not_grow_any_of_the_seven(self) -> None: + """None of the seven mints on a miss -- each is a closed set on the + bare lookup tier, not the mutating + :class:`~pcapkit.corekit.enum.EnumRegistry` one. Exercises every + ``get`` (name hit, value hit, name miss, value miss) and then + re-checks every size against the values pinned above, in the same + process and without subprocess isolation: unlike a registry whose + ``_missing_`` mints a placeholder, every one of these seven either + resolves an existing member or raises, so there is nothing for an + earlier assertion in this method to leave behind for a later one to + mistake for growth. + """ + from pcapkit.const.ftp.command import CommandType, ConformanceRequirement + from pcapkit.protocols.internet.esp import ESPStatus + from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus, + IPv6AddressPrefixCode, LMAAddressCode, + LocalizedRoutingStatus) + + before = { + CommandType: (4, 3), ConformanceRequirement: (3, 3), ESPStatus: (6, 6), + FastBindingAcknowledgmentStatus: (6, 6), IPv6AddressPrefixCode: (4, 4), + LocalizedRoutingStatus: (3, 3), LMAAddressCode: (3, 3), + } + for cls, (members, count) in before.items(): + with self.subTest(cls=cls.__name__): + self.assertEqual(len(cls.__members__), members) + self.assertEqual(len(list(cls)), count) + + member = next(iter(cls)) + self.assertIs(cls.get(member.name), member) + self.assertIs(cls.get(member.value), member) + with self.assertRaises(KeyError): + cls.get('NOT_A_REAL_MEMBER_930') + with self.assertRaises(ValueError): + cls.get(1 << 70) + + self.assertEqual(len(cls.__members__), members) + self.assertEqual(len(list(cls)), count) + + +class GetByNameAndValueTests(unittest.TestCase): + """``get`` resolves by name and by value on each of the seven, whether + the ``get`` reached is the base's own or one of the two kept overrides.""" + + def test_command_type(self) -> None: + from pcapkit.const.ftp.command import CommandType + + self.assertIs(CommandType.get('A'), CommandType.A) + self.assertIs(CommandType.get(1), CommandType.A) + + def test_conformance_requirement(self) -> None: + from pcapkit.const.ftp.command import ConformanceRequirement + + self.assertIs(ConformanceRequirement.get('M'), ConformanceRequirement.M) + self.assertIs(ConformanceRequirement.get(1), ConformanceRequirement.M) + + def test_esp_status(self) -> None: + from pcapkit.protocols.internet.esp import ESPStatus + + self.assertIs(ESPStatus.get('DECRYPTED'), ESPStatus.DECRYPTED) + self.assertIs(ESPStatus.get(0), ESPStatus.DECRYPTED) + self.assertIs(ESPStatus.get('UNSUPPORTED'), ESPStatus.UNSUPPORTED) + self.assertIs(ESPStatus.get(5), ESPStatus.UNSUPPORTED) + + def test_fast_binding_acknowledgment_status(self) -> None: + from pcapkit.protocols.internet.mh import FastBindingAcknowledgmentStatus + + self.assertIs( + FastBindingAcknowledgmentStatus.get('Fast_Binding_Update_accepted'), + FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted) + self.assertIs(FastBindingAcknowledgmentStatus.get(0), + FastBindingAcknowledgmentStatus.Fast_Binding_Update_accepted) + + def test_ipv6_address_prefix_code(self) -> None: + from pcapkit.protocols.internet.mh import IPv6AddressPrefixCode + + self.assertIs(IPv6AddressPrefixCode.get('Old_Care_of_Address'), + IPv6AddressPrefixCode.Old_Care_of_Address) + self.assertIs(IPv6AddressPrefixCode.get(1), IPv6AddressPrefixCode.Old_Care_of_Address) + + def test_localized_routing_status(self) -> None: + from pcapkit.protocols.internet.mh import LocalizedRoutingStatus + + self.assertIs(LocalizedRoutingStatus.get('Success'), LocalizedRoutingStatus.Success) + self.assertIs(LocalizedRoutingStatus.get(0), LocalizedRoutingStatus.Success) + + def test_lma_address_code(self) -> None: + from pcapkit.protocols.internet.mh import LMAAddressCode + + self.assertIs(LMAAddressCode.get('Reserved'), LMAAddressCode.Reserved) + self.assertIs(LMAAddressCode.get(0), LMAAddressCode.Reserved) + + +class FailedLookupTests(unittest.TestCase): + """What a miss raises on each of the seven -- a name miss is + :exc:`KeyError`-derived and a value miss :exc:`ValueError`-derived, + matching stdlib :class:`~enum.Enum`'s own shape (GitHub issue #923) on + every one of the seven regardless of whether its ``get`` is the base's + own or a kept :class:`staticmethod` override. + """ + + def test_command_type(self) -> None: + from pcapkit.const.ftp.command import CommandType + + with self.assertRaises(KeyError) as name_miss: + CommandType.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + CommandType.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_conformance_requirement(self) -> None: + from pcapkit.const.ftp.command import ConformanceRequirement + + with self.assertRaises(KeyError) as name_miss: + ConformanceRequirement.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + ConformanceRequirement.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_esp_status(self) -> None: + from pcapkit.protocols.internet.esp import ESPStatus + + with self.assertRaises(KeyError) as name_miss: + ESPStatus.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + ESPStatus.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_fast_binding_acknowledgment_status(self) -> None: + """Its own kept ``get`` raises :exc:`EnumKeyError` directly for a + name miss (GitHub issue #923's conversion, left untouched by this + change) and delegates to :meth:`_missing_` for a value miss, which + always raises :exc:`EnumValueError`.""" + from pcapkit.protocols.internet.mh import FastBindingAcknowledgmentStatus + + with self.assertRaises(KeyError) as name_miss: + FastBindingAcknowledgmentStatus.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + FastBindingAcknowledgmentStatus.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_ipv6_address_prefix_code(self) -> None: + from pcapkit.protocols.internet.mh import IPv6AddressPrefixCode + + with self.assertRaises(KeyError) as name_miss: + IPv6AddressPrefixCode.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + IPv6AddressPrefixCode.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_localized_routing_status(self) -> None: + """A pure re-parent whose own :meth:`_missing_` always raises + :exc:`EnumValueError` directly -- the base's ``get`` re-raises it + unchanged rather than wrapping it, since it is already a + :exc:`~pcapkit.utilities.exceptions.BaseError`.""" + from pcapkit.protocols.internet.mh import LocalizedRoutingStatus + + with self.assertRaises(KeyError) as name_miss: + LocalizedRoutingStatus.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + LocalizedRoutingStatus.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + def test_lma_address_code(self) -> None: + from pcapkit.protocols.internet.mh import LMAAddressCode + + with self.assertRaises(KeyError) as name_miss: + LMAAddressCode.get('NOT_A_REAL_MEMBER') + self.assertIsInstance(name_miss.exception, EnumKeyError) + + with self.assertRaises(ValueError) as value_miss: + LMAAddressCode.get(1 << 70) + self.assertIsInstance(value_miss.exception, EnumValueError) + + +class KeptOverrideQuietnessTests(unittest.TestCase): + """The now-quiet raise of the two kept ``get`` overrides, and the new + way this re-parenting opens to reach it. + + :meth:`FastBindingAcknowledgmentStatus.get` and + :meth:`IPv6AddressPrefixCode.get` used to raise **loud** on a name + miss -- logging once at :data:`logging.CRITICAL` and setting + :data:`sys.tracebacklimit` to ``0`` process-wide, unlike the base's own + quiet raise at :meth:`~pcapkit.corekit.enum.EnumLookup.get` + (:mod:`pcapkit.corekit.enum`). GitHub issue #930 converges both onto + that quiet shape instead, settled on GitHub issue #933's follow-up + ruling, verbatim: *"Oh wait. I meant, they should follow house + convention and not to be loud."* (An earlier message on the same issue + said the opposite -- plain *"No."* -- and an earlier revision of this + file briefly pinned loud as the settled answer on the strength of that + message; the follow-up four minutes later superseded it, and what + follows is the corrected version.) + + Both halves are pinned quiet, and for two different reasons against the + reverted tree. :meth:`test_get_is_now_quiet_on_both_classes` covers + ``get`` itself, which existed and was already loud on the tree before + this issue -- so this test fails against that reverted tree not because + ``get`` is structurally different there, but because its behaviour + genuinely changes: reverted, it is loud; here, it is quiet. + :meth:`test_get_all_is_new_and_quiet_too` covers + :meth:`~pcapkit.corekit.enum.EnumLookup.get_all`, which did not exist + on either class before #930 at all and is now inherited from the base, + calling this same ``get`` internally -- so a miss reached through + ``get_all`` is quiet for the same reason ``get`` itself is, and this + test fails against the reverted tree with ``AttributeError`` before it + ever reaches the quietness assertion. + """ + + def setUp(self) -> None: + self._saved_tracebacklimit = getattr(sys, 'tracebacklimit', None) + + def tearDown(self) -> None: + if self._saved_tracebacklimit is None: + if hasattr(sys, 'tracebacklimit'): + del sys.tracebacklimit + else: + sys.tracebacklimit = self._saved_tracebacklimit + + def test_get_is_now_quiet_on_both_classes(self) -> None: + """The behaviour GitHub issue #930 actually changes: ``get`` was + loud on the tree before this issue (see the class docstring) and + is quiet now, matching the base.""" + from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus, + IPv6AddressPrefixCode) + + for cls in (FastBindingAcknowledgmentStatus, IPv6AddressPrefixCode): + with self.subTest(cls=cls.__name__): + if hasattr(sys, 'tracebacklimit'): + del sys.tracebacklimit + with capture(logger) as recorder: + with self.assertRaises(EnumKeyError): + cls.get('NOT_A_REAL_MEMBER') + self.assertFalse(hasattr(sys, 'tracebacklimit')) + self.assertEqual(recorder.messages, []) + + def test_get_all_is_new_and_quiet_too(self) -> None: + """The half this issue adds outright: ``get_all`` did not exist on + either class before, and the miss it now reaches through this same + ``get`` is quiet for the same reason ``get`` itself is.""" + from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus, + IPv6AddressPrefixCode) + + for cls in (FastBindingAcknowledgmentStatus, IPv6AddressPrefixCode): + with self.subTest(cls=cls.__name__): + # Not merely present: it resolves through this class's own + # kept ``get`` rather than some other path -- ``get_all`` + # calls ``cls.get(key)`` and wraps the canonical member in a + # one-item tuple (see ``EnumLookup.get_all``). + member = next(iter(cls)) + self.assertEqual(cls.get_all(member.name), (member,)) + + if hasattr(sys, 'tracebacklimit'): + del sys.tracebacklimit + with capture(logger) as recorder: + with self.assertRaises(EnumKeyError): + cls.get_all('NOT_A_REAL_MEMBER') + self.assertFalse(hasattr(sys, 'tracebacklimit')) + self.assertEqual(recorder.messages, []) + + +class PureReparentClassmethodTests(unittest.TestCase): + """The five pure re-parents inherit the base's ``classmethod`` outright, + having defined no ``get`` of their own to begin with -- unlike + :class:`FastBindingAcknowledgmentStatus` and + :class:`IPv6AddressPrefixCode`, whose kept ``get`` overrides stay + :class:`staticmethod` and are pinned alongside their base-tuple change in + :class:`ReparentedBasesTests` instead, since a test that only checked the + decorator would pass identically before and after this change and pin + nothing about it. + """ + + def test_the_five_pure_reparents_inherit_the_bare_classmethod(self) -> None: + from pcapkit.const.ftp.command import CommandType, ConformanceRequirement + from pcapkit.protocols.internet.esp import ESPStatus + from pcapkit.protocols.internet.mh import LMAAddressCode, LocalizedRoutingStatus + + for cls in (CommandType, ConformanceRequirement, ESPStatus, + LocalizedRoutingStatus, LMAAddressCode): + with self.subTest(cls=cls.__name__): + self.assertIsInstance(inspect.getattr_static(cls, 'get'), classmethod) + # Inherited, not redeclared: the class's own ``__dict__`` carries no + # ``get`` of its own, which is the difference between a pure + # re-parent and a kept-and-converted override. + self.assertNotIn('get', vars(cls)) + + +class ZeroRemainOutsideEnumLookupTests(unittest.TestCase): + """The census this issue exists to finish: a runtime walk over every + importable :mod:`pcapkit.*` module (vendor templates excluded), filtering + on ``issubclass(cls, EnumLookup)``, must now find **zero** enumerations + outside the hierarchy -- the assertion that stops this recurring a + fourth time. Nested classes are walked too, since GitHub issue #877's own + count of 24 non-registry enumerations includes the seven ``httpv2`` + ``Flags`` classes nested inside schema classes. + """ + + def test_the_walk_found_something_to_count(self) -> None: + """Guards the assertion below from passing on an empty discovery.""" + self.assertGreater(len(self._every_enum()), 100) + + def test_zero_enumerations_remain_outside_the_hierarchy(self) -> None: + outside = [name for cls, name in self._every_enum().items() + if not issubclass(cls, EnumLookup)] + self.assertEqual(outside, [], f'still outside EnumLookup: {sorted(outside)}') + + @staticmethod + def _every_enum() -> 'dict[type, str]': + """Every :class:`~enum.Enum`/:class:`~aenum.Enum` :mod:`pcapkit` defines, + nested ones included, skipping :mod:`pcapkit.vendor`. + + Returns: + Each enumeration mapped to its dotted qualified name. + + """ + import enum + + import aenum + + import pcapkit + + for module_info in pkgutil.walk_packages(pcapkit.__path__, 'pcapkit.'): + if module_info.name.startswith('pcapkit.vendor'): + continue + importlib.import_module(module_info.name) + + found = {} # type: dict[type, str] + visited = set() # type: set[type] + + def walk(container: 'type') -> None: + if container in visited: + return + visited.add(container) + for value in vars(container).values(): + if not isinstance(value, type): + continue + if not getattr(value, '__module__', '').startswith('pcapkit'): + continue + if value.__module__.startswith('pcapkit.vendor'): + continue + if issubclass(value, (enum.Enum, aenum.Enum)): + found.setdefault(value, f'{value.__module__}.{value.__qualname__}') + walk(value) + + import sys + + for name, module in list(sys.modules.items()): + if not name.startswith('pcapkit') or name.startswith('pcapkit.vendor'): + continue + for value in vars(module).values(): + if not isinstance(value, type): + continue + if not getattr(value, '__module__', '').startswith('pcapkit'): + continue + if issubclass(value, (enum.Enum, aenum.Enum)): + found.setdefault(value, f'{value.__module__}.{value.__qualname__}') + walk(value) + return found + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/project/test_conventions_doc_claims.py b/tests/project/test_conventions_doc_claims.py index 9fa31a8f7..19afcfac4 100644 --- a/tests/project/test_conventions_doc_claims.py +++ b/tests/project/test_conventions_doc_claims.py @@ -66,9 +66,11 @@ #: Number words as the page spells them, so a count can be read back out of the prose. #: The page states its figures in words rather than digits, which is house style there. +#: ``'zero'`` joined the set with GitHub issue #930, once phase 2 finished and the +#: outside-the-hierarchy count it counts dropped to none. NUMBER_WORDS = { - 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, - 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, + 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, + 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, } @@ -244,8 +246,14 @@ def test_the_walk_found_something_to_count(self) -> 'None': 'pass vacuously') def test_the_page_states_the_measured_number_outside_the_hierarchy(self) -> 'None': - """*"Seven are still outside the hierarchy"* -- against a runtime walk.""" - stated = re.search(r'\*\*(\w+) are still outside the hierarchy\*\*', self.note) + """*"Zero enumerations remain outside the hierarchy"* -- against a runtime walk. + + GitHub issue #930 finished phase 2, so the page's own wording moved from + *"are still outside"* -- true while seven remained -- to *"remain outside"*, + which reads correctly now that none do. + """ + stated = re.search(r'\*\*(\w+) enumerations remain outside the hierarchy\*\*', + self.note) self.assertIsNotNone(stated, 'the page no longer states how many enumerations ' 'are outside EnumLookup; the wording this test ' 'reads has changed') @@ -255,13 +263,31 @@ def test_the_page_states_the_measured_number_outside_the_hierarchy(self) -> 'Non f'{len(self.outside)}: {sorted(self.outside.values())}') def test_the_page_names_every_enumeration_outside_the_hierarchy(self) -> 'None': - """The count alone would pass on a wrong list of the right length.""" + """The count alone would pass on a wrong list of the right length. + + Vacuous while :attr:`self.outside` is empty -- GitHub issue #930 emptied + it, and an empty dict gives the loop below nothing to iterate, so this + method cannot fail no matter what the page says right now. + ``test_the_page_states_the_measured_number_outside_the_hierarchy`` is + what actually pins the empty state; this one is dormant rather than + deleted, for whenever a future regression makes :attr:`self.outside` + non-empty again, at which point it resumes checking that the page names + each one. :meth:`~unittest.TestCase.skipTest` says so explicitly rather + than passing silently. + """ + if not self.outside: + self.skipTest('self.outside is empty (GitHub issue #930); nothing to check') for name in sorted(self.outside.values()): with self.subTest(enumeration=name): self.assertIn(name.rsplit('.', maxsplit=1)[-1], self.note) def test_the_page_states_the_measured_phase_two_progress(self) -> 'None': - """*"landed for 17 of the 24 non-registry enumerations"*, both figures.""" + """*"landed for 24 of the 24 non-registry enumerations"*, both figures. + + 17 of 24 until GitHub issue #930 finished phase 2's remaining seven -- + ``CommandType``, ``ConformanceRequirement``, ``ESPStatus`` and the four + Mobility Header helpers -- so both figures now read the same. + """ stated = re.search(r'landed for (\d+) of the (\d+) non-registry enumerations', self.note) self.assertIsNotNone(stated, 'the page no longer states phase 2 progress in ' diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index 8d06b98c1..3779b021e 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -1634,13 +1634,28 @@ def test_mh_local_enums_raise_and_do_not_alias(self) -> None: from pcapkit.protocols.misc.raw import Raw from pcapkit.utilities.exceptions import EnumValueError - with self.subTest('LocalizedRoutingStatus and LMAAddressCode raise and have no get()'): + with self.subTest('LocalizedRoutingStatus and LMAAddressCode raise and now have get()'): # Both had zero callers repo-wide (tests included), so GitHub issue - # #880 deleted ``get()`` outright rather than rebuilding it. + # #880 deleted the hand-rolled ``get()`` outright rather than + # rebuilding it on the immutable contract -- true on the tree #880 + # left behind, where neither class carried ``get`` at all. GitHub + # issue #930 re-parented both onto + # :class:`~pcapkit.corekit.enum.EnumLookup`, which is what gives + # every non-registry enumeration ``get``/``get_all`` for free, so + # ``hasattr`` now reads the other way -- the immutable contract + # itself is what this subtest still pins: the base's own ``get`` + # still cannot mint, so an unassigned value raises there exactly + # as it does through the bare constructor. + from pcapkit.utilities.exceptions import EnumKeyError + for enum_cls, unassigned in ((LocalizedRoutingStatus, 50), (LMAAddressCode, 50)): with self.assertRaises(EnumValueError): enum_cls(unassigned) - self.assertFalse(hasattr(enum_cls, 'get')) + self.assertTrue(hasattr(enum_cls, 'get')) + with self.assertRaises(EnumValueError): + enum_cls.get(unassigned) + with self.assertRaises(EnumKeyError): + enum_cls.get('NOT_A_REAL_MEMBER') with self.subTest('the shared -1 aliasing defect is gone'): # Before the fix, ``get('bogus_one')`` minted a member at the