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
81 changes: 72 additions & 9 deletions pcapkit/const/reg/apptype/apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from aenum import IntEnum, StrEnum, auto, extend_enum

from pcapkit.corekit.enum import EnumRegistry
from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup, EnumRegistry

__all__ = ['TransportProtocol', 'AppType']

Expand All @@ -25,7 +25,7 @@
from pcapkit.corekit.multidict import MultiDict


class TransportProtocol(IntEnum):
class TransportProtocol(EnumLookup, IntEnum):
"""Transport layer protocol."""

# mypy has no aenum plugin, so this class is a plain class to it: every
Expand Down Expand Up @@ -87,19 +87,70 @@ class TransportProtocol(IntEnum):
#: Datagram Congestion Control Protocol.
dccp = cast('TransportProtocol', auto())

@staticmethod
def get(key: 'int | str') -> 'TransportProtocol':
@classmethod
def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtocol':
"""Backport support for original codes.

Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for GitHub
issue #877's re-parenting, but keeps this override rather than
dropping it -- two behaviours the base does not reproduce on its own:

* **Case folding.** This class has always matched a name
case-insensitively (``key.lower()``); the base's own ``str``
branch is case-sensitive. Lowering ``key`` before delegating
reproduces that: every member name here is already lower-case, so
a lowered ``key`` still hits the base's exact ``_member_map_``
lookup.
* **The refusal.** Maintainer ruling on GitHub PR #836: "Do not
allow extension of TransportProtocol at all." The base's own miss
on a ``str`` key raises a bare :exc:`KeyError`; this class has
always raised :exc:`ValueError` naming the rejected key, which is
what every caller and test here already depends on, so a name
miss is caught and re-raised in that shape rather than left as the
base's own exception.

The base is a :class:`classmethod`
(:meth:`~pcapkit.corekit.enum.EnumLookup.get`), so this override
moves from :class:`staticmethod` to :class:`classmethod` to
delegate at all -- the same move GitHub issue #908 and #915 made for
:meth:`~pcapkit.const.http.method.Method.get`. Grepped every call
site in this tree for GitHub issue #877: all call this method by
name, none take it as a bare callable or introspect ``__func__``,
so the switch is not caller-visible.

``default`` did not exist on this override before this change --
the original had no such parameter at all, which is a genuine LSP
violation once this class' ``get`` is a :class:`classmethod`
override of one that has it: a caller holding a
:class:`~pcapkit.corekit.enum.EnumLookup`-typed reference could pass
``default=`` and, before this, would have hit a
:exc:`TypeError` at this subclass. Forwarded verbatim to
:meth:`~pcapkit.corekit.enum.EnumLookup.get` rather than
reimplemented, so it behaves exactly as the base's own ``default``
does: a fallback to an *already-registered* value, resolved through
``_value2member_map_`` and never through the constructor, so
passing one still cannot mint. Every call site in this tree omits
it, so this is purely an added, backward-compatible capability, not
a change to anything this tree exercises today.

Args:
key: Key to get enum item.
default: An already-registered value to fall back to when
``key`` resolves to nothing. :data:`~pcapkit.corekit.enum.
NO_DEFAULT`, the default, means *no default* -- see
:meth:`~pcapkit.corekit.enum.EnumLookup.get`.

Raises:
ValueError: If ``key`` names no member, by name or by value, and
there is no usable ``default``.

:meta private:
"""
if isinstance(key, int):
return TransportProtocol(key)
if key.lower() in TransportProtocol.__members__:
return TransportProtocol[key.lower()] # type: ignore[misc]
if isinstance(key, str):
try:
return super().get(key.lower(), default)
except KeyError:
raise ValueError(f'{key!r} is not a valid {cls.__name__}') from None
# NOTE: maintainer ruling on this PR (#836): "Do not allow extension
# of TransportProtocol at all." A name that is not a declared member
# used to mint a brand-new one here, at ``max_val + 1`` (before that,
Expand All @@ -117,7 +168,19 @@ def get(key: 'int | str') -> 'TransportProtocol':
# splitting." A ``'|'``-joined name is therefore not special any
# more -- it is simply not the name of a declared member, and gets
# the same message as any other one that is not.
raise ValueError(f'{key!r} is not a valid {TransportProtocol.__name__}')
#
# NOTE: the delegation below is exception-compatible for the keys this
# signature admits -- an unrecognised :class:`int` still reaches the
# caller as the same plain :exc:`ValueError`. It is not compatible for
# keys outside it: ``None``, a :class:`float` and an unhashable key
# used to raise :exc:`AttributeError` from the ``key.lower()`` this
# branch no longer reaches, and now raise :exc:`ValueError` (or, for a
# :class:`float`, resolve -- ``get(1.0)`` answers ``tcp``, since
# ``cls(key)`` accepts whatever :class:`int` equality accepts). No
# caller in this tree can reach any of those: the only live call site
# passes ``proto.lower()``, always a :class:`str`. The new shape is
# what every other ``EnumLookup`` subclass already does.
return super().get(key, default)

# NOTE: ``_missing_`` used to range-check ``value`` and then defer to
# :mod:`aenum`'s own ``Flag._missing_``, which is what composed an
Expand Down
12 changes: 10 additions & 2 deletions pcapkit/corekit/infoclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import itertools
from typing import TYPE_CHECKING, Generic, TypeVar

from pcapkit.corekit.enum import EnumLookup
from pcapkit.utilities.compat import Mapping, final
from pcapkit.utilities.exceptions import InfoError, UnsupportedCall, stacklevel
from pcapkit.utilities.warnings import InfoWarning, warn
Expand All @@ -31,8 +32,15 @@
ST = TypeVar('ST', bound='Type[Info]')


class FinalisedState(enum.IntEnum):
"""Finalised state."""
class FinalisedState(EnumLookup, enum.IntEnum):
"""Finalised state.

Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue #877's ruling that every non-registry enumeration shares that
lookup contract -- pure re-parenting, since this class defines neither
``get`` nor ``_missing_`` of its own to reconcile with the base.

"""

#: Not finalised.
NONE = enum.auto()
Expand Down
8 changes: 7 additions & 1 deletion pcapkit/foundation/reassembly/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from typing import TYPE_CHECKING

from pcapkit.corekit.enum import EnumLookup
from pcapkit.corekit.infoclass import Info, info_final
from pcapkit.utilities.compat import StrEnum, auto

Expand All @@ -17,9 +18,14 @@
from pcapkit.protocols.protocol import ProtocolBase


class Completion(StrEnum):
class Completion(EnumLookup, StrEnum):
"""How completely a datagram was reassembled, and why it stopped.

Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue #877's ruling that every non-registry enumeration shares that
lookup contract -- pure re-parenting, since this class defines neither
``get`` nor ``_missing_`` of its own to reconcile with the base.

This is the value of
:attr:`Datagram.completed <pcapkit.foundation.reassembly.data.ip.Datagram.completed>`.
That field used to be a plain :obj:`bool`, and this enumeration is a widening
Expand Down
12 changes: 10 additions & 2 deletions pcapkit/protocols/application/ftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from pcapkit.const.ftp.command import Command as Enum_Command
from pcapkit.const.ftp.return_code import ReturnCode as Enum_ReturnCode
from pcapkit.corekit.enum import EnumLookup
from pcapkit.protocols.application.application import Application
from pcapkit.protocols.data.application.ftp import FTP as Data_FTP
from pcapkit.protocols.data.application.ftp import Request as Data_Request
Expand All @@ -37,8 +38,15 @@
FTP_RESPONSE = re.compile(rb'^(?P<code>[0-9]{3})(?P<more>\-)?( +(?P<args>.*))?\r\n$', re.I)


class Type(StrEnum):
"""FTP packet type."""
class Type(EnumLookup, StrEnum):
"""FTP packet type.

Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue #877's ruling that every non-registry enumeration shares that
lookup contract -- pure re-parenting, since this class defines neither
``get`` nor ``_missing_`` of its own to reconcile with the base.

"""

#: Request packet.
REQUEST = auto()
Expand Down
12 changes: 10 additions & 2 deletions pcapkit/protocols/application/httpv1.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from pcapkit.const.http.method import Method as Enum_Method
from pcapkit.const.http.status_code import StatusCode as Enum_StatusCode
from pcapkit.corekit.enum import EnumLookup
from pcapkit.corekit.multidict import OrderedMultiDict
from pcapkit.protocols.application.http import HTTP as HTTPBase
from pcapkit.protocols.data.application.httpv1 import HTTP as Data_HTTP
Expand Down Expand Up @@ -137,8 +138,15 @@ def _test_start_line(data: 'bytes') -> 'bool':
)


class Type(StrEnum):
"""HTTP packet type."""
class Type(EnumLookup, StrEnum):
"""HTTP packet type.

Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue #877's ruling that every non-registry enumeration shares that
lookup contract -- pure re-parenting, since this class defines neither
``get`` nor ``_missing_`` of its own to reconcile with the base.

"""

#: Request packet.
REQUEST = auto()
Expand Down
106 changes: 84 additions & 22 deletions pcapkit/protocols/application/ngap.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@

from pcapkit.const.ngap.procedure_code import ProcedureCode as Enum_ProcedureCode
from pcapkit.const.ngap.protocol_ie import ProtocolIE as Enum_ProtocolIE
from pcapkit.corekit.enum import NO_DEFAULT, EnumLookup
from pcapkit.corekit.infoclass import Info
from pcapkit.protocols.application.application import Application
from pcapkit.protocols.data.application.ngap import IE as Data_IE
Expand Down Expand Up @@ -193,12 +194,17 @@ def load_pycrate() -> 'Optional[Any]':
##############################################################################


class PDUKind(StrEnum):
class PDUKind(EnumLookup, StrEnum):
"""Which alternative of the ``NGAP-PDU`` ``CHOICE`` a PDU is.

The values are spelled as the ASN.1 identifiers, so that a name decoded by
|pycrate|_ resolves by value.

Re-parented onto :class:`~pcapkit.corekit.enum.EnumLookup` per GitHub
issue #877's ruling that every non-registry enumeration shares that
lookup contract -- pure re-parenting, since this class defines neither
``get`` nor ``_missing_`` of its own to reconcile with the base.

"""

#: A procedure's request, or a class 2 procedure's only message.
Expand All @@ -209,7 +215,7 @@ class PDUKind(StrEnum):
UNSUCCESSFUL_OUTCOME = 'unsuccessfulOutcome'


class Criticality(IntEnum):
class Criticality(EnumLookup, IntEnum):
"""[Criticality] What a receiver must do with an IE it does not understand.

Members are named for the ASN.1 identifiers rather than upper-cased, so
Expand All @@ -226,43 +232,99 @@ class Criticality(IntEnum):
#: Ignore the IE, carry on, and report it.
notify = 2

@staticmethod
def get(key: 'int | str | Criticality') -> 'Criticality':
@classmethod
def get(cls, key: 'int | str | Criticality', default: 'Any' = NO_DEFAULT) -> 'Criticality':
"""Backport support for original codes.

Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for GitHub
issue #877's re-parenting. For every key the signature admits, the
base reproduces the branch this override used to hand-roll: a
``Criticality`` key is also an :class:`int` (this is an
:class:`~aenum.IntEnum`) and resolves through the base's non-``str``
path, ``cls(key)``, which -- exactly like the removed
``isinstance(key, Criticality): return key`` branch -- hands back the
identical, canonical member rather than a new one; a plain
:class:`str` resolves through the base's name lookup, the same
``Criticality[key]`` this override used to spell directly.

Outside that signature the two do differ, which is worth stating
rather than leaving for someone to discover. ``cls(key)`` accepts
anything :class:`int` equality accepts, so ``get(1.0)`` now returns
``Criticality.ignore`` where the removed code raised
:exc:`ValueError`, and an unhashable key raises :exc:`ValueError`
rather than the :exc:`TypeError` the old ``Criticality[key]`` lookup
produced. Both are out of contract, no caller in this tree can reach
them -- the three live call sites pass pycrate-decoded :class:`str`
or :class:`int` -- and the new shape is what every other
:class:`~pcapkit.corekit.enum.EnumLookup` subclass already does, so
this is alignment rather than a regression.

The one behaviour the base does not reproduce is the exception this
class has always raised for an unresolved name: a bare
:exc:`KeyError` there, versus this class's own :exc:`ValueError`
naming the rejected key -- so a name miss is still caught and
re-raised in that shape. An unresolved *value* is unaffected either
way: it already reaches the caller as :exc:`ValueError`, raised by
:meth:`_missing_` below, on both the removed code path and the
base's.

The base is a :class:`classmethod`
(:meth:`~pcapkit.corekit.enum.EnumLookup.get`), so this override
moves from :class:`staticmethod` to :class:`classmethod` to
delegate at all -- the same move GitHub issue #908 and #915 made for
:meth:`~pcapkit.const.http.method.Method.get`. Every call site in
this tree calls this method by name; none take it as a bare
callable or introspect ``__func__``, so the switch is not
caller-visible.

Unlike :meth:`ProcedureCode.get <pcapkit.corekit.enum.EnumRegistry.get>`
and :meth:`ProtocolIE.get <pcapkit.corekit.enum.EnumRegistry.get>`,
this takes no ``default``. Those two answer an in-range value they have
not seen with a throwaway, non-registering member (see
:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`), which
is the right answer for a registry 3GPP keeps assigning new codes to.
``Criticality`` cannot grow. It is an ASN.1 ``ENUMERATED`` with no
extension marker, so a fourth value is unencodable and a lookup for one
is a bug rather than a version skew -- see :meth:`_missing_`. A
``default`` parameter here would have to be ignored, and one that is
declared, documented and ignored is worse than one that is absent.
this still never manufactures a member for an in-range value it has
not seen -- ``Criticality`` cannot grow, unlike those two, which
answer with a throwaway, non-registering member (see
:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`) for
a registry 3GPP keeps assigning new codes to. It is an ASN.1
``ENUMERATED`` with no extension marker, so a fourth value is
unencodable and a lookup for one is a bug rather than a version skew
-- see :meth:`_missing_`.

``default`` did not exist on this override before this change --
the original had no such parameter at all, which is a genuine LSP
violation once this class' ``get`` is a :class:`classmethod`
override of one that has it (mypy's ``[override]`` check catches
exactly this shape). Forwarded verbatim to
:meth:`~pcapkit.corekit.enum.EnumLookup.get` rather than
reimplemented, so it behaves exactly as the base's own ``default``
does: a fallback to an *already-registered* member, resolved
through ``_value2member_map_`` and never through the constructor,
so passing one still cannot mint a fourth. Every call site in this
tree omits it, so this is purely an added, backward-compatible
capability -- not the "declared, documented and ignored" shape an
earlier revision of this docstring rejected, since it is now
genuinely honoured rather than a parameter that would have to be
silently dropped.

Args:
key: Key to get enum item.
default: An already-registered value to fall back to when
``key`` resolves to nothing. :data:`~pcapkit.corekit.enum.
NO_DEFAULT`, the default, means *no default*.

Returns:
The matching member.

Raises:
ValueError: If ``key`` names no member. Raised for an unknown name as
well as an unknown value, so that the two ways of getting this
wrong do not report differently.
ValueError: If ``key`` names no member and there is no usable
``default``. Raised for an unknown name as well as an
unknown value, so that the two ways of getting this wrong
do not report differently.

:meta private:
"""
if isinstance(key, Criticality):
return key
if isinstance(key, int):
return Criticality(key)
try:
return Criticality[key] # type: ignore[misc]
return super().get(key, default)
except KeyError:
raise ValueError('%r is not a valid %s' % (key, Criticality.__name__)) from None
raise ValueError('%r is not a valid %s' % (key, cls.__name__)) from None

@classmethod
def _missing_(cls, value: 'int') -> 'NoReturn':
Expand Down
Loading
Loading