fix(const): give the six unguarded registries the same bare-ValueError guard - #677
Conversation
Records the six constant registries that rejected an invalid value in a way the built-in `enum` does not -- three composing a pseudo-member for any integer at all -- and why the issue's own proposal to raise `EnumError` was rejected rather than adopted. Also corrects the #623 entry above, which stated that `pcapkit/const/tcp/flags.py` "defines no `_missing_`". It defined none at the time and never had #623's recursion defect, which is what that sentence was about, but it has one now; the tense is fixed and the new entry named. `CHANGELOG.md` regenerated with `python util/changelog_md.py`; `--check` exits 0.
…r guard Of the 117 generated modules under `pcapkit/const/`, 113 reject an invalid value with a bare `ValueError`, which is what the built-in `enum.IntEnum` raises. That is the intended behaviour and is left alone; the defect is the four modules that did something else. Recounted by execution rather than by grep, at registry rather than module granularity, the divergence is six registries in those four modules: - `tcp/flags.py::Flags`, `ftp/command.py::CommandType` and `reg/apptype.py::TransportProtocol` are `IntFlag` classes that defined no `_missing_` at all, so `aenum`'s `Flag` machinery composed a pseudo-member for any integer whatsoever. `Flags(-1)` returned 65520, the OR of every declared TCP header flag, so a value no 16-bit wire field can hold read back as every flag set at once; `Flags(-65536)` read back as none set. - `ftp/command.py::Command`, `ftp/command.py::FEATCode` and `http/method.py::Method` are `StrEnum` classes whose `_missing_` reached `value.upper()` before checking the type, so an integer raised `AttributeError: 'int' object has no attribute 'upper'`. All six now raise a bare `ValueError`. The flag guards hand in-range composites to `super()._missing_` rather than resolving them, so #623's unbounded recursion stays fixed. The `_missing_` -> `extend_enum` register fallback of the mutable registries is untouched and now pinned by test. `pcapkit/vendor/tcp/flags.py` declared `FLAG = '4 <= value <= 15'` while its template never interpolated it. Those are the registry's *bit offsets* and the members are `1 << offset`, so emitting it unchanged would have rejected every composite; it now reads `0 <= value <= 0xFFFF`, the field those bits live in. `TransportProtocol` reads its bound off its own members because `TransportProtocol.get` extends the registry at runtime. Fixed in the four bespoke templates under `pcapkit/vendor/` and regenerated; `pcapkit/vendor/default.py` needed no change, since the 113 it emits were already correct. The regeneration is 42 insertions and 0 deletions across exactly those 4 of 134 files, so the committed tree was not stale. `tests/const/test_const_enum_get.py` drops `Flags` from `EXPECTED_TO_RESOLVE_ANYTHING` and its sweep grows from 110 to 111, since a registry that bounds its domain has a failure for `default` to fall back from. tests/const: 40 passed, 798 subtests, exit 0. Coverage of the changed files rises, measured on the pre-fix tree with the pre-fix tests and on this one: `const/tcp/flags.py` 73% -> 100%, `const/ftp/command.py` 87% -> 97%, `const/http/method.py` 81% -> 96%. Fixes #647
dc5d17c to
919fa3b
Compare
|
GOOD TO GO Cross-review by an independent subagent on a different model ( It raised one required fix, and it was right: the commit message quoted the coverage "before" figures as 95/92/90, which were an intermediate measurement of the fixed tree rather than the pre-fix baseline. The correct figures are the ones in the PR body — 73% → 100%, 87% → 97%, 81% → 96% — which the reviewer reproduced exactly and independently. Fixed by amending the commit message; the tree is byte-identical ( What it verified independently, and how:
Two things it flagged without calling them defects, recorded here rather than folded away:
One process note the review surfaced that is worth recording: the worktree was briefly checked out to an unrelated commit mid-review, because the same worktree was being used to stage the changelog entry onto #657's branch via a detached HEAD. The reviewer detected it from the reflog, discarded its first census as invalid, and redid everything against immutable snapshots. It made no modifications itself. The worktree is now clean on CI is not being claimed green: every Actions job on this PR is currently |
Resolves the inconsistency #647 counted, in the direction the owner set: "keep const enums mock the actual default built-in enum's behaviour, with one exception: they contain the missing then register fallback (mutable enums)".
So the 113 modules that raise a bare
ValueErrorare correct and untouched — that is exactly whatenum.IntEnumraises for a value it does not define. The issue's proposal to swap inEnumErroris not taken:EnumErroris(BaseError, TypeError)atpcapkit/utilities/exceptions.py:276and is not aValueError, so it would have diverged from the built-in and sailed past theexcept ValueErrorin all 113 generatedget()bodies, silently undoing #584.The real counts, recounted rather than taken from the issue
117 generated modules (134
.pyless 17__init__.py), and 123 registries in them — 111 non-flagIntEnum, 7IntFlag, 5StrEnum. The issue's 113/117 is right at module granularity and at the level of the guard's source text. Measured behaviour at registry granularity finds more, because two of the four unguarded modules hold several registries and one guarded module holds an unguarded helper:file:line(pre-fix)Cls(-1)didFlagspcapkit/const/tcp/flags.py:2265520—Reserved_4|…|FIN, every declared flag at onceCommandTypepcapkit/const/ftp/command.py:537—A|P|STransportProtocolpcapkit/const/reg/apptype.py:2615—tcp|udp|sctp|dccpCommandpcapkit/const/ftp/command.py:304AttributeError: 'int' object has no attribute 'upper'FEATCodepcapkit/const/ftp/command.py:43AttributeErrorMethodpcapkit/const/http/method.py:182AttributeErrorSix registries, four modules. The first three defined no
_missing_at all, soaenum'sFlagmachinery composed a pseudo-member for any integer;Flags(-65536)read back as no flags set. The last three reachedvalue.upper()before checking the type.After the change all 123 of 123 registries raise a bare
ValueErrorforCls(-1).Two modules are text-level outliers that are deliberately left alone, because their observable behaviour is already correct:
pcapkit/const/ipv6/extension_header.py:20(ExtensionHeader) andpcapkit/const/ftp/command.py:66(ConformanceRequirement) define no_missing_andaenumraises the bareValueErrorfor them. Adding a redundant guard would be churn.The fix is in the templates, not the tree
pcapkit/vendor/default.pyneeded no change — the 113 it emits were already right. The guard lives in four bespoke templates, one per unguarded registry:pcapkit/vendor/tcp/flags.py— itsLINEemitted no_missing_, and itsFLAGchecker was declared but never interpolated. It readisinstance(value, int) and 4 <= value <= 15: those are the registry's bit offsets, while the members it generates are1 << offset. Emitting it unchanged would have rejected every composite, every member above bit 3, andFlags(0)— so it now reads0 <= value <= 0xFFFF, the 16-bit field those bits live in.pcapkit/vendor/ftp/command.py— a guard forCommandType(0 <= value <= 0x07) and a type guard forFEATCodeandCommand.pcapkit/vendor/http/method.py— a type guard forMethod.pcapkit/vendor/reg/apptype.py— a guard forTransportProtocol, whose bound is read offcls.__members__rather than written down, becauseTransportProtocol.getextends the registry at runtime atmax * 2; a literal0x0Fwould reject the very member it had just grown. Pinned bytest_transport_protocol_can_still_be_extended_at_runtime.The flag guards end in
return super()._missing_(value), the shape #632 landed for the Mobility Header registries, so in-range composites still decompose and #623's unbounded recursion stays fixed.Then regenerated with the real crawler,
python -m pcapkit.vendor tcp.flags ftp.command http.method reg.apptype, exit 0, no warnings.The rest of
pcapkit/const/is byte-identicalgit diff --numstatover the regenerated tree is 42 insertions and 0 deletions, in exactly 4 of the 134 files:Zero deletions is the proof: the live IANA registries still render the committed text character for character, including the 30k-line
reg/apptype.py. CI's follow-up pass,isort -l100 -ppcapkit pcapkit/const/*/*.py, exits 0 and changes nothing. Nothing was stale before this change.The register fallback is preserved
The owner's one named exception. Unchanged and now pinned by
ConstEnumRegisterFallbackTests:Method('FROBNICATE'),Command('XYZZY'),FEATCode('<zzzz>')still register a new member;Method('get')is still a case-insensitive hit that registers nothing.ProtectionAuthority(1<<70)andCGAType(1<<70)stillextend_enum— and still reject-1, which no unassigned span covers.AppType.get(65000, proto=tcp)still registers the port, i.e. get()'s documented default is ignored on the integer path across the shared const/ enum template #584's machinery through_missing_is intact.Tests
New:
tests/const/test_const_enum_builtin_parity.py— 40 tests over the whole tier, including the direct built-in-equivalence assertion the owner's direction is really about (test_the_exception_type_matches_the_built_in_enum: a const registry and a stdlibenum.IntEnummust raise the same type for the same invalid value), a test thatEnumErroris specifically not what is raised, the register-fallback pins above, and a character-for-character render ofpcapkit/vendor/tcp/flags.pyagainst the committed module so a regeneration cannot revert the guard.One nuance recorded honestly in the test rather than glossed:
enum.IntFlagdefaults toboundary=KEEPand composes an out-of-range value rather than raising, so the flag registries' parity is against the built-in's own reject mode,boundary=STRICT— same exception type either way, which is the property at issue.Failing without the fix, passing with it, exit codes read from files rather than from a pipeline:
One existing pin moved, and it is the sweep this change belongs in:
tests/const/test_const_enum_get.pydropsFlagsfromEXPECTED_TO_RESOLVE_ANYTHINGandcoveredgoes 110 → 111, because a registry that bounds its domain now has a failure fordefaultto fall back from.EXPECTED_FAILURESintests/protocols/test_option_roundtrip_unit.pywas imported rather than grepped (it is built with**unpacking): 45 entries, none moved.Coverage rises on every changed file:
pcapkit/const/tcp/flags.pypcapkit/const/ftp/command.pypcapkit/const/http/method.pytests/vendor/55 passed,tests/protocols/test_option_roundtrip_unit.py+tests/foundation/registry/15 passed / 438 subtests, all exit 0. The*_runtime.pyfailures intests/protocols/transport/andtests/protocols/application/are allFileNotFoundError: sample capture … not foundfor uncommitted generated fixtures — pre-existing and unrelated (zero non-FileNotFoundErrorfailures in those runs).Why not
breakingConsidered and rejected. Every input whose result changes is an input that cannot occur:
-1,-65536,65536and1<<70are not representable on the wire.pcapkit/protocols/transport/tcp.pyconstructs onlyFlags(0)and composites of defined bits, and every one of those still resolves.StrEnumregistries the change isAttributeError→ValueError, i.e. a crash becomes the documented rejection. CatchingAttributeErrorout of an enum lookup is not a contract.get(key, default)callers gain behaviour rather than losing it: the guard'sValueErroris what makes thedefaultfall back, where before a junk pseudo-member was returned.Labels:
fix,test.Out of scope, noted not touched
The generated
get()methods carrydefault: 'int' = -1and compare it with==rather thanis, dispatched on across module boundaries atpcapkit/const/l2tpv2.py:237andpcapkit/const/ospf.py:191. That is a behaviour change of its own and is left alone here.Also left alone:
docs/source/changelog/1.5.0.rst:1123andCHANGELOG.md:81assert in prose thatpcapkit/const/tcp/flags.py"defines no_missing_", which this change makes false. No changelog file is touched on this branch by standing rule; the correction goes to #657 with the entry.Fixes #647