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
4 changes: 4 additions & 0 deletions docs/source/pcapkit/utilities/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ It is still an ordinary exception carrying its message, so ``except`` clauses an
:no-members:
:show-inheritance:

.. autoexception:: pcapkit.utilities.exceptions.EnumKeyError
:no-members:
:show-inheritance:

:exc:`ModuleNotFoundError` Category
-----------------------------------

Expand Down
58 changes: 36 additions & 22 deletions pcapkit/const/reg/apptype/apptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,33 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco
"""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:
issue #877's re-parenting, but keeps this override rather than dropping
it, for one behaviour 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.

* **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.
Case folding is now the *only* thing this override adds. It used to
convert the base's name-miss exception as well -- this class raised
:exc:`ValueError` where the base raised :exc:`KeyError` -- and GitHub
issue #923's ruling retired that conversion: *"Either ``ValueError``
or ``KeyError``, that's depending on how stdlib's ``Enum`` would raise
on these circumstances."* A stdlib ``E['nosuch']`` raises
:exc:`KeyError`, and #923's census of the 127 concrete
:class:`~pcapkit.corekit.enum.EnumLookup` subclasses -- taken before
#921 re-parented this class, so this class is not among them -- found
119 already answering a name miss that way against 5 answering with
:exc:`ValueError`. Those 5 are :class:`AppType` and its four transport
registries, and they land there only because their own ``get()`` takes
an :class:`int` port and never accepts a name at all, rather than from
any name-miss policy. So there was no policy here to preserve, and a
name miss now reaches the caller as
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` from the base.
Maintainer ruling on GitHub PR #836 -- "Do not allow extension of
TransportProtocol at all" -- is untouched by that: the refusal is still
a refusal and still mints nothing, only its exception class moved.

The base is a :class:`classmethod`
(:meth:`~pcapkit.corekit.enum.EnumLookup.get`), so this override
Expand Down Expand Up @@ -141,16 +152,17 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco
:meth:`~pcapkit.corekit.enum.EnumLookup.get`.

Raises:
ValueError: If ``key`` names no member, by name or by value, and
there is no usable ``default``.
EnumKeyError: If ``key`` names no member and there is no usable
``default``. A :exc:`KeyError`, from the base, since GitHub
issue #923 -- it used to be a plain :exc:`ValueError` raised
here.
EnumValueError: If ``key`` is a value no member carries and there
is no usable ``default``. A :exc:`ValueError`, from the base.

:meta private:
"""
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
return super().get(key.lower(), default)
# 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 @@ -171,7 +183,9 @@ def get(cls, key: 'int | str', default: 'Any' = NO_DEFAULT) -> 'TransportProtoco
#
# 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
# caller as a :exc:`ValueError`, now the base's own
# :exc:`~pcapkit.utilities.exceptions.EnumValueError` since GitHub issue
# #923 rather than :mod:`aenum`'s bare one. 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
Expand Down
77 changes: 64 additions & 13 deletions pcapkit/corekit/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
from aenum import extend_enum

from pcapkit.corekit.sentinels import NO_DEFAULT, NoDefaultType # pylint: disable=unused-import
from pcapkit.utilities.exceptions import BaseError, EnumKeyError, EnumValueError

if TYPE_CHECKING:
from typing import Any
Expand Down Expand Up @@ -190,7 +191,12 @@ def _validate_value(cls, value: 'Any') -> 'None':
:meth:`get`'s own ``except ValueError`` and falls back to ``default``
just as any other unresolvable value does. An override raising something
outside that hierarchy would instead propagate past ``default``, which is
a real difference in behaviour rather than a stylistic preference.
a real difference in behaviour rather than a stylistic preference. With
no usable ``default``, a rejection from this hook reaches the caller
exactly as the override raised it -- :meth:`get` re-raises an in-library
``ValueError`` unchanged rather than re-wrapping it, so the override's own
message and the single log record it already emitted are what the caller
sees.

Called from exactly two places, and the omissions are deliberate:

Expand Down Expand Up @@ -330,6 +336,46 @@ def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'Self':
on the same terms; the ``str`` path does not call the hook, for the reason
given on :meth:`_validate_value` itself.

Both failure paths raise from :mod:`pcapkit.utilities.exceptions` rather
than a builtin, per the owner's ruling on GitHub issue #923: *"Either
``ValueError`` or ``KeyError``, that's depending on how stdlib's
``Enum`` would raise on these circumstances. And we should raise one
from ``pcapkit.utilities.exceptions`` rather builtin exceptions."* The
*shape* is unchanged by that ruling and deliberately so -- a name miss
stays :exc:`KeyError`-derived and a value miss :exc:`ValueError`-derived,
matching ``E['nosuch']`` and ``E(999)`` on a stdlib
:class:`~enum.Enum`, and matching the 119 of this tree's 127 concrete
subclasses that already answered a name miss that way. Only the
provenance changed, so every ``except KeyError`` and ``except
ValueError`` around a call to this method keeps catching.

Two details of that conversion are worth stating, since neither is
visible from the exception type alone:

* **The name miss is raised quietly** --
:exc:`~pcapkit.utilities.exceptions.EnumKeyError` with ``quiet=True``,
so nothing is logged and :data:`sys.tracebacklimit` is left alone.
That is not a cosmetic choice: this method's name miss is in-library
control flow at six call sites, and at
:meth:`~pcapkit.const.http.method.Method.get` it is part of a
*successful* call -- that override catches it in order to mint. A loud
error there would put a :data:`logging.CRITICAL` record on every such
call and set :data:`sys.tracebacklimit` to ``0`` process-wide, which
is exactly the GitHub issue #362 defect
:class:`~pcapkit.utilities.exceptions.BaseError` documents ``quiet``
for. The value miss takes no such fallback anywhere in this tree, so
it stays loud.
* **An in-library rejection propagates unchanged.** A ``ValueError``
that is already a :exc:`~pcapkit.utilities.exceptions.BaseError` --
typically :exc:`~pcapkit.utilities.exceptions.EnumValueError` from a
subclass's :meth:`_validate_value` -- is re-raised as it stands rather
than wrapped, so the subclass's own message survives and the error is
logged once instead of twice. Only :mod:`aenum`'s and :mod:`enum`'s
own "no member carries this value" is converted. This is the same
discrimination :meth:`EnumField.post_process
<pcapkit.corekit.fields.numbers.EnumField.post_process>` already
makes for the same reason.

Args:
key: Name or value to look up.
default: An already-registered value to fall back to when
Expand All @@ -344,13 +390,15 @@ def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'Self':
The canonical member for ``key``, or for ``default``.

Raises:
ValueError: If a value does not resolve and there is no usable
default -- including a value a subclass's
:meth:`_validate_value` rejects, since
:exc:`~pcapkit.utilities.exceptions.EnumValueError` is a
:exc:`ValueError`.
KeyError: If a name does not resolve and there is no usable
default.
EnumValueError: If a value does not resolve and there is no usable
default. Also what a subclass's :meth:`_validate_value`
rejection reaches the caller as, since that hook is documented
to raise this very class and it is passed through rather than
re-wrapped. A :exc:`ValueError`, so an
``except ValueError`` caller is unaffected.
EnumKeyError: If a name does not resolve and there is no usable
default. A :exc:`KeyError`, so an ``except KeyError`` caller is
unaffected.

"""
if isinstance(key, str):
Expand All @@ -360,14 +408,17 @@ def get(cls, key: 'Any', default: 'Any' = NO_DEFAULT) -> 'Self':
if key in cls._value2member_map_:
return cls._value2member_map_[key]
if default is NO_DEFAULT or default not in cls._value2member_map_:
raise
raise EnumKeyError(f'{key!r} is not a valid {cls.__name__}',
quiet=True) from None
return cls._value2member_map_[default]
try:
cls._validate_value(key)
return cls(key) # type: ignore[call-arg]
except ValueError:
except ValueError as error:
if default is NO_DEFAULT or default not in cls._value2member_map_:
raise
if isinstance(error, BaseError):
raise
raise EnumValueError(str(error)) from error
return cls._value2member_map_[default]

@classmethod
Expand All @@ -391,8 +442,8 @@ def get_all(cls, key: 'Any') -> 'tuple[Self, ...]':
carrying the same value.

Raises:
ValueError: As :meth:`get` with no default, for a value.
KeyError: As :meth:`get` with no default, for a name.
EnumValueError: As :meth:`get` with no default, for a value.
EnumKeyError: As :meth:`get` with no default, for a name.

"""
canonical = cls.get(key)
Expand Down
Loading
Loading