Skip to content

fix(const): value-lookup in Method.get, so BASELINE-CONTROL/VERSION-CONTROL resolve (#908) - #915

Merged
JarryShaw merged 1 commit into
mainfrom
fix/908-method-get-value-lookup
Sep 29, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/908-method-get-value-lookup

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • fix — corrects a defect

Description of your pull request and other information

Closes #908. Method.get checked only _member_map_ (names), never _value2member_map_ (values), so the two methods whose name differs from their value -- BASELINE_CONTROL/'BASELINE-CONTROL' and VERSION_CONTROL/'VERSION-CONTROL' -- fell through to an unregistered member with the wrong safe/idempotent. httpv1.py's permissive method regex ([A-Z][A-Z-]*) lets a real capture reach this, so a successful parse silently reported wrong metadata.

Delegated to the base EnumLookup.get, keeping the override only for the unregistered-member fallback -- the issue's own suggested one-liner needed get to move from staticmethod to classmethod first, since zero-argument super() has no first argument to bind otherwise. Kept default's existing meaning (the unregistered member's value) rather than the base's NO_DEFAULT/registered-value-only semantics, since that would be caller-visible.

Case-sensitivity (#896/#907) and caller's-own-casing (#860) are unchanged, each pinned by its own test. Fixed in the crawler template (pcapkit/vendor/http/method.py) and the generated file together, verified byte-identical by rendering the template against the committed file. Added an end-to-end httpv1 test pinning the real parse path, plus a template/generated-file parity test so a regeneration can't drop the fix.

@JarryShaw JarryShaw added bug Issues reporting a defect (set by the bug report template; a default, not an assessment) fix Pull requests that fix a defect (fix: subject prefix) const Regenerated IANA or vendor constant tables; members keep their numeric values review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 29, 2026
@JarryShaw
JarryShaw force-pushed the fix/908-method-get-value-lookup branch from c59ee55 to d6663df Compare September 29, 2026 06:53
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review on opus (author sonnet) returned NEEDS CHANGES, and it was right. Fixed in d6663df38; the PR now stands at review: pending until CI clears, and the finding is worth recording because it is the kind CI cannot see.

except KeyError is narrower than what the base raises. Delegating routes a non-str key through cls(key) → _missing_ → ValueError, which the override does not catch. I re-measured on both trees rather than taking the report:

              origin/main e38d695ae            this PR
get(42)       AttributeError: no 'upper'   ->  ValueError: 42 is not a valid Method
get(None)     AttributeError: no 'upper'   ->  ValueError: None is not a valid Method
get(b'GET')   RETURNED name=b'GET' value=b'GET'  ->  ValueError
tables        (40, 40) -> (40, 40) on both — no minting either way

The bytes row is a return-to-raise change, not merely a different exception type, and bytes is the plausible mistake here: _RE_METHOD is a bytes pattern and httpv1 is bytes throughout, so the live call at httpv1.py:434 is safe only because it wraps the match in self.decode(...). Raising is the right behaviour — a bytes-valued Method is not something this registry should hand back — but it shipped with no Raises: clause, no test, and no mention, in the same commit that declined to widen default expressly because that would be caller-visible. Fixed with a Raises: clause and a new NonStrKeyTests, all four of which fail against unmodified main, in the crawler template as well as the generated module.

The reviewer also corrected me on the default divergence, and I had it backwards. I framed #915 as creating a divergence with #913. In fact #913 puts two contracts inside one file deliberately — FEATCode.get at NO_DEFAULT, Command.get left at default=None, with a test section headed "Contrast, pinned so the default is not quietly widened". The real axis is new override versus pre-existing override, and #915 makes the same conservative call #913 does. A third contract already predates both: pcapkit/const/pcapng/option_type.py:238 carries default: 'int' = -1. So after both land there are four distinct contracts across five overrides, none of them introduced here. A tree-wide default audit would be worth having, but it is not this PR's job.

Confirmed clean by the reviewer with its own derivation: the staticmethod→classmethod switch is caller-invisible (no bare-callable use, no subclass of Method, and the __func__ introspection at test_const_registry_protocol.py:177,902 walks only CONVERTED and GENERATED_SAMPLE, which exclude Method); name-vs-value precedence is inert — across all 40 members there is no string that is one member's name and a different member's value; template parity holds to one trailing newline the test's own normaliser re-adds; and the 8-of-12 pre-change failure count re-derives exactly.

Unverified and stated: the author's 100% coverage figure did not reproduce at the reviewer's narrower scope (94%, missing lines 57/312/316), and test_docstring_contract.py plus test_const_registry_protocol.py were cut at the time budget after ~9 minutes with no output.

…ONTROL resolve (#908)

`Method.get` checked only `_member_map_` (names), never `_value2member_map_`
(values), so `BASELINE_CONTROL`/`'BASELINE-CONTROL'` and
`VERSION_CONTROL`/`'VERSION-CONTROL'` -- whose name and value differ, a
hyphen being unusable in an identifier -- fell through to an unregistered
member with the wrong `safe`/`idempotent`. `httpv1.py`'s permissive method
regex lets a real capture reach this.

- Delegate to the base `EnumLookup.get`, keeping the override only for the
  unregistered fallback; moved `staticmethod` -> `classmethod` since
  zero-argument `super()` needs `cls` to bind.
- Kept `default`'s existing meaning (the fallback member's value) rather
  than the base's `NO_DEFAULT`/registered-value-only semantics, which would
  be caller-visible.
- Case-sensitivity (#896/#907) and caller's-own-casing (#860) are
  unchanged, each pinned by its own test; fixed in the crawler template and
  the generated file together, verified byte-identical.

Added an end-to-end `httpv1` test and a template/generated-file parity test.

Build: `coverage run -m unittest` over `tests/const/` and
`test_http_unit.py`, all passing.

Document and pin the non-`str` surface, which this fix moves as a side
effect. Delegating to the base routes a non-`str` key through `cls(key)` and
so `_missing_`, which raises `ValueError`; `except KeyError` catches only a
failed *name* lookup, so that `ValueError` reaches the caller. Measured
before and after:

    get(42)      AttributeError: 'int' object has no attribute 'upper'
                 -> ValueError: 42 is not a valid Method
    get(None)    AttributeError  -> ValueError
    get(b'GET')  RETURNED name=b'GET' value=b'GET'
                 -> ValueError: b'GET' is not a valid Method

The bytes row is a return-to-raise change, and bytes is the plausible
mistake: `_RE_METHOD` is a bytes pattern and `httpv1` is bytes throughout,
so `httpv1.py:434` is safe only because it wraps the match in
`self.decode(...)`. Raising is the intended behaviour -- a bytes-valued
`Method` is not something this registry should hand back -- so the fix is a
`Raises:` clause plus four pins in a new `NonStrKeyTests`, all four of which
fail against unmodified main (failures=2, errors=2; four distinct method
names). The docstring pin reads the source via `inspect.getsource(Method.get)`
rather than `Method.get.__func__`, which a `staticmethod` does not carry --
against the pre-fix module the `__func__` form died with `AttributeError`
before any assertion ran, so it pinned `classmethod`-ness rather than the
clause it is named for. Added to the crawler template as well as the
generated module, so regeneration cannot drop it.
@JarryShaw
JarryShaw force-pushed the fix/908-method-get-value-lookup branch from d6663df to c6695e0 Compare September 29, 2026 07:02
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO — the re-review closed its own finding on d6663df38, and I have made one further fix it flagged in my test, so the head is now c6695e019.

It attacked the hole I asked about — a non-str key where cls(key) might fail before _missing_ — and could not find one. aenum catches the unhashable TypeError internally, falls through to a linear search, then _missing_, so the isinstance guard always fires:

['GET']  {'a':1}  {1,2}  bytearray(b'GET')  memoryview(b'GET')  3.14  1+2j  object()
  -> ValueError every time;  final tables (40, 40)
Method overrides _validate_value: False   (so it is the base's no-op)

It also confirmed str subclasses and Method members themselves take the str path and resolve normally, which is correct and matches the base's isinstance(key, str) — so the clause is accurate as written. tests/test_docstring_contract.py passes, and not by luck: unreachable_exceptions() only flags a clause when the body is judged incapable of raising, and super().get(key) is not on its inert-call list, so a clause documenting a callee's exception is correctly exempt. Its two earlier UNVERIFIED suites now pass too — test_const_registry_protocol + test_const_enum_get, 82 tests, OK.

Coverage rose on exactly the line raised: pcapkit/const/http/method.py 94% → 96%, with line 312 — _missing_'s non-str guard, the line the fix newly makes reachable from get — now covered. The vendor file's misses shifted by exactly +14 (248→262, 262-264→276-278, 296→310), which independently confirms the only vendor change is the 14 docstring lines inside the LINE template.

And it was right about a weakness in a test I wrote, so I fixed it rather than accept "not worth a revision". My test_the_docstring_documents_the_value_error used Method.get.__func__, which a staticmethod does not carry — so against the pre-fix module it died with AttributeError before any assertion ran. It was pinning classmethod-ness, already covered by DefaultSignatureTests, rather than the clause it is named for. Now it uses inspect.getsource(Method.get), which works on both descriptor kinds, and asserts a distinctive sentence instead of the bare word ValueError, which was near-vacuous over 4 kB of docstring. Verified against a pristine origin/main worktree: it now fails with AssertionError: 'Raises:' not found. The pre-change tally moves from failures=1, errors=3 to failures=2, errors=2 — still four distinct method names.

Unverified and stated: the author's original 100% coverage figure at its own wider scope was never reproduced (96% at the reviewer's narrower scope, residual lines explained); the Sphinx build against the rewritten docstrings; and test_const_enum_builtin_parity.py / test_const_enum_no_mint.py, both of which call Method.get.

@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 b1d4c4a into main Sep 29, 2026
63 checks passed
@JarryShaw
JarryShaw deleted the fix/908-method-get-value-lookup branch September 29, 2026 13: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
…up (#877)

Phase 2 of #877, the 11 classes across 8 files not held by #913/#904:
TransportProtocol, FinalisedState, Completion, ftp.Type, httpv1.Type,
Criticality, PDUKind, PacketDirection, PacketReception, WireGuardKeyLabel,
and FrameType.Flags (carrying its 6 per-frame subclasses transitively).
Each now mixes in EnumLookup ahead of its enum base for the shared
get/get_all contract.

TransportProtocol and Criticality already had their own get, both as a
staticmethod against EnumLookup.get's classmethod (the #908/#915 trap).
Both are now classmethods delegating to super().get(), keeping only what
the base does not reproduce -- TransportProtocol's case-fold and no-mint
refusal, Criticality's case-sensitive miss -- each re-raised as the
ValueError callers already depend on rather than the base's KeyError.
Each gained a default parameter forwarded to the base, since dropping one
the base declares is a real classmethod-override violation under mypy.

Updated test_const_enum_get.py's exclusion set for TransportProtocol's
new default, and added test_enum_lookup_reparent_877_unit.py pinning the
re-parenting, both preserved overrides, and no member-table growth.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…up (#877)

Phase 2 of #877, the 11 classes across 8 files not held by #913/#904:
TransportProtocol, FinalisedState, Completion, ftp.Type, httpv1.Type,
Criticality, PDUKind, PacketDirection, PacketReception, WireGuardKeyLabel,
and FrameType.Flags (carrying its 6 per-frame subclasses transitively).
Each now mixes in EnumLookup ahead of its enum base for the shared
get/get_all contract.

TransportProtocol and Criticality already had their own get, both as a
staticmethod against EnumLookup.get's classmethod (the #908/#915 trap).
Both are now classmethods delegating to super().get(), keeping only what
the base does not reproduce -- TransportProtocol's case-fold and no-mint
refusal, Criticality's case-sensitive miss -- each re-raised as the
ValueError callers already depend on rather than the base's KeyError.
Each gained a default parameter forwarded to the base, since dropping one
the base declares is a real classmethod-override violation under mypy.

Updated test_const_enum_get.py's exclusion set for TransportProtocol's
new default, and added test_enum_lookup_reparent_877_unit.py pinning the
re-parenting, both preserved overrides, and no member-table growth.
JarryShaw added a commit that referenced this pull request Sep 29, 2026
…up (#877) (#921)

Phase 2 of #877, the 11 classes across 8 files not held by #913/#904:
TransportProtocol, FinalisedState, Completion, ftp.Type, httpv1.Type,
Criticality, PDUKind, PacketDirection, PacketReception, WireGuardKeyLabel,
and FrameType.Flags (carrying its 6 per-frame subclasses transitively).
Each now mixes in EnumLookup ahead of its enum base for the shared
get/get_all contract.

TransportProtocol and Criticality already had their own get, both as a
staticmethod against EnumLookup.get's classmethod (the #908/#915 trap).
Both are now classmethods delegating to super().get(), keeping only what
the base does not reproduce -- TransportProtocol's case-fold and no-mint
refusal, Criticality's case-sensitive miss -- each re-raised as the
ValueError callers already depend on rather than the base's KeyError.
Each gained a default parameter forwarded to the base, since dropping one
the base declares is a real classmethod-override violation under mypy.

Updated test_const_enum_get.py's exclusion set for TransportProtocol's
new default, and added test_enum_lookup_reparent_877_unit.py pinning the
re-parenting, both preserved overrides, and no member-table growth.
@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

bug Issues reporting a defect (set by the bug report template; a default, not an assessment) const Regenerated IANA or vendor constant tables; members keep their numeric values fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(const): Method.get never checks values, so BASELINE-CONTROL and VERSION-CONTROL mis-resolve

1 participant