Skip to content

fix(corekit,utilities): raise pcapkit exceptions from EnumLookup.get, in stdlib Enum's shape (#923) - #927

Merged
JarryShaw merged 1 commit into
mainfrom
fix/923-enum-get-exception-provenance
Sep 29, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/923-enum-get-exception-provenance

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • fix — corrects a defect
  • feat — adds a feature
  • perf — changes performance, not behaviour
  • refactor — changes neither behaviour nor performance
  • test — tests only
  • docs — documentation only
  • ci — workflows or build tooling
  • chore — anything else

Description of your pull request and other information

Closes #923.

EnumKeyError(BaseError, KeyError) is added rather than MissingKeyError reused: that one reports an absent mapping key (MultiDict, both toolkit extractors), and conflating it with "no member carries this name" would break the distinction toolkit/scapy.py relies on when it converts one into the other.

Because it derives from KeyError, the blast radius is nil — all six in-library except KeyError sites keep catching, and Method.get still mints. Only provenance changed: the shape (name miss → KeyError, value miss → ValueError) already matched stdlib and 119 of the census's 127 subclasses.

Two judgement calls worth a reviewer's eye:

  • The name miss is raised quiet=True. It is control flow at six sites and at Method.get part of a successful call, so a loud error would log CRITICAL and set sys.tracebacklimit = 0 on every mint — the MultiDict.get() on an absent key logs an ERROR record per lookup #362 defect. The value miss is never absorbed that way, so it stays loud. Both pinned, with a loud control.
  • An in-library ValueError passes through unwrapped, so a _validate_value override's message survives and logs once; only aenum's bare one is converted. Same discrimination EnumField.post_process already makes.

docs/source/contributing/conventions.rst still documents the old contract and needs updating; #918 owns that file, so nothing here touches it. Details in the issue.

… in stdlib Enum's shape

Closes #923. The owner's ruling: "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."

- Add `EnumKeyError(BaseError, KeyError)`, the name-miss sibling of
  `EnumValueError`. Deriving from `KeyError` keeps every existing
  `except KeyError` catching -- `Method.get` catches it in order to mint, so
  for that one a name miss is part of a successful call.
- `EnumLookup.get` now raises `EnumKeyError` (quietly, since the name miss is
  in-library control flow at six sites) and `EnumValueError`, keeping the
  stdlib shape. An in-library `ValueError` from `_validate_value` passes
  through unwrapped.
- Drop the now-wrong `KeyError` -> `ValueError` conversion from
  `TransportProtocol.get` (and its vendor template twin), which keeps only
  its `.lower()` call, and delete `Criticality.get`, whose body became a pure
  pass-through.
- `mh.py`'s two name-miss raises become `EnumKeyError`: right provenance
  already, wrong shape under this ruling.

Tested: 4 new test classes plus 4 more in
tests/corekit/test_enum_get_exception_provenance_923_unit.py; mypy and pylint
clean on the changed files; no existing `assertRaises(KeyError)` needed editing.
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) test Pull requests that add or correct tests (test: subject prefix) const Regenerated IANA or vendor constant tables; members keep their numeric values docs Pull requests that change documentation only (docs: subject prefix) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 29, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

My own verification, ahead of the cross-review verdict — posting the measurements rather than waiting, since the most load-bearing claim here is a negative one that is easy to assert and easy to get wrong.

364 tests green in two batches, run under the repo venv with PYTHONPATH forced to the branch worktree:

new suite + test_enum_lookup_base_unit + test_const_registry_protocol + test_const_enum_no_mint
  Ran 207 tests in 132.524s ... OK
the four edited test files + test_isort_clean
  Ran 157 tests in  88.560s ... OK

That settles the claim that mattered: no existing assertRaises(KeyError) needed editing. tests/corekit/test_enum_lookup_base_unit.py pins the base's contract directly and tests/const/test_const_registry_protocol.py states it in prose at lines 1258-1261 — both pass untouched. Had either needed a change, the design would have been wrong rather than the test.

Also measured directly:

EnumKeyError.__mro__       -> EnumKeyError, BaseError, KeyError, LookupError
except KeyError catches    -> EnumKeyError: "'definitely-nosuch' is not a valid TransportProtocol"
                              EnumKeyError: "'definitely-nosuch' is not a valid Criticality"
Criticality has own get    -> False
TransportProtocol.get('TCP') -> tcp
Method.get('FROBNICATE-923')  -> minted; _member_map_/_value2member_map_ 40 -> 40
  log output during mint   -> ''   (empty)

The last line is what makes the quiet=True choice defensible rather than merely convenient: minting is a successful call, and a loud BaseError there would emit CRITICAL and set sys.tracebacklimit = 0 process-wide on every mint. Empty output means today's observable behaviour is preserved exactly, since the bare builtin logged nothing either.

This is not the verdict. A Sonnet cross-review is running (the author was Opus), and its assignment is the two judgement calls rather than these measurements: whether quiet=True on the name miss costs a caller too much debuggability, and whether the unwrapped-in-library-ValueError rule has the precedent it claims at pcapkit/corekit/fields/numbers.py:625-631. review: pending stands until that lands.

@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO at a08a4898c — Sonnet cross-review (author was Opus), and it independently confirmed both judgement calls rather than deferring to them. My own 364-test verification is two comments up.

The quiet=True asymmetry is structural, not arbitrary — the reviewer established why, which I had only assumed. A name miss is a bare _member_map_[key] dict lookup with no interception hook, so a quiet KeyError-derived exception is the only way to keep the six in-library catch sites working. A value miss is intercepted upstream, inside _missing_, which either mints or raises — so nothing in the tree ever catches a ValueError out of get() to declare success. "Never absorbed into a success" is therefore true by construction rather than by omission.

It also sharpened the debuggability argument in the opposite direction to my worry: quiet=True does not suppress the exception or alter its message. Making it loud would be worse for an uncaught escape, because setting sys.tracebacklimit = 0 process-wide blanks tracebacks for every subsequent unrelated exception — the #362 defect verbatim.

The precedent for the unwrapped in-library ValueError is real. pcapkit/corekit/fields/numbers.py:620-631 does the same except ValueError as error: if isinstance(error, BaseError): raise before falling through to _pseudo_member. Not an invented rule.

One stale number, not a defect, and it is mine. The "28 assertRaises(KeyError) across 8 files" figure in #923's census was taken at 3766c3c09. At this PR's base 5e0ec3889 it is 29 across 9, because #921 later added tests/corekit/test_enum_lookup_reparent_877_unit.py, contributing one more pre-existing assertion at line 242 (FinalisedState.get) that is unrelated to #923 and untouched here. Worth correcting since I quoted the census twice.

Also confirmed independently: MissingKeyError reuse would have made toolkit/scapy.py:342 a no-op self-catch; the vendor template needs no brace escaping because the replacement contains no f-string at all, so it is true by the shape of the change rather than by luck; the pre-fix split is exactly 19 failing / 14 passing, and all 14 were spot-checked as genuine invariants rather than tests that would pass against a broken implementation.

Two things the reviewer left UNVERIFIED and I am not treating as closed: the package-wide mypy figure (321 errors / 38 files) was not re-derived — only the six changed files were, identically on both trees — and pylint on those six nets one fewer finding with no new category, tracing the C0209 claim to Criticality._missing_ at the renumbered ngap.py:270.

review: good-to-go granted. Unmerged and unpublished.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 29, 2026
@JarryShaw
JarryShaw merged commit 4f3d43d into main Sep 29, 2026
63 checks passed
@JarryShaw
JarryShaw deleted the fix/923-enum-get-exception-provenance branch September 29, 2026 19:08
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 29, 2026
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…se (#918) (#929)

* Retitle *Registry Conventions* -> *House Conventions* and widen the preamble:
  the page now carries a protocol-class ruling as well as `pcapkit.const` ones,
  and records the standing ask that a ruling is written here in the same change
  that implements it.
* Correct the five passages #927 left for this issue: the `FEATCode` name miss
  raises `EnumKeyError` rather than a bare `KeyError`; a `_validate_value`
  rejection propagates unwrapped with no usable `default`; #877's phase 2 has
  landed for 17 of the 24 non-registry enumerations rather than "not happened
  yet"; `TransportProtocol.get` is now only a case fold and `Criticality.get` is
  gone.
* New "What a Failed Lookup Raises" for #923's provenance-and-shape ruling.
* New "Which bases an IPv6 extension header names" for #924's subclassing ruling,
  the RFC census behind it, the retired `IPv6_GenericExt` name, and why ESP is an
  extension header that still cannot short-circuit the chain walk.
* Document `EnumValueError`, which had no `autoexception` entry, so five
  references to it on this page rendered as plain text.

15 new tests pin the checkable claims. tests/project 193 OK,
test_sentinel_exports_unit 18 OK, test_ipv6_ext_unit + FEATCode + enum-lookup-base
77 OK; docs build clean, every new cross-reference resolved in the rendered HTML.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) const Regenerated IANA or vendor constant tables; members keep their numeric values docs Pull requests that change documentation only (docs: subject prefix) fix Pull requests that fix a defect (fix: subject prefix) test Pull requests that add or correct tests (test: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(corekit,utilities): raise pcapkit exceptions from EnumLookup.get, following stdlib Enum's shape

1 participant