You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The generated _missing_ range guards under pcapkit/const/ raise a bare ValueError, in 113 of the 117 modules. pcapkit.utilities.exceptions.EnumError exists for exactly this and is entirely unused — three references in the whole tree, none of them a raise. And one of the four modules that is exempt from the count is exempt for the worst possible reason: pcapkit/const/tcp/flags.py carries no guard at all, so Flags(-1) returns every defined bit instead of rejecting a value no wire field can hold.
Two defects in one census, filed together because the same sweep produces both and tcp/flags.py appears in each — as an exemption in the first and as the subject of the second.
Measured on 375e9d411, CPython 3.14.7, aenum 3.1.17, tree asserted against the worktree rather than the editable install (pcapkit.__file__ = …/pcapkit/__init__.py printed on every run). Every file named below is byte-identical on current origin/main (da381f259), verified with git diff --stat 375e9d411..origin/main -- <path>, and the census was re-run against both trees and returns the same 113/117 in each — so nothing here is stale against the tree as it stands.
Part 1 — 113 of 117 modules raise a bare ValueError
117 is 134 .py files less 17 __init__.py. All 117 carry the generated-provenance banner. An AST pass resolving every raise to its enclosing function gives:
modules (excl __init__.py): 117
files with >=1 ValueError raise: 113
total raise sites: 114
_missing_ Call 113
get bare 1
113 is the number to quote, not 114. The 114th, pcapkit/const/reg/apptype.py:30605, is a bare raise ValueError used as internal control flow and caught two lines later, so it never escapes:
The 113 real sites are byte-identical, one per module, all in _missing_. Verbatim, with the guard above it — pcapkit/const/arp/hardware.py:180-181:
ifnot (isinstance(value, int) and0<=value<=65535):
raiseValueError('%r is not a valid %s'% (value, cls.__name__))
Same line at pcapkit/const/ipv4/option_number.py:143, pcapkit/const/mh/packet.py:125, pcapkit/const/reg/apptype.py:30623, and 109 others. The {FLAG} guard differs per registry — 90 distinct _missing_ shapes — while the raise line does not vary at all.
The four exempt modules, for two different reasons
Module
Why exempt
Bare ValueError still reaches a caller?
pcapkit/const/ftp/command.py
StrEnum; _missing_ auto-extends, no failure path
No
pcapkit/const/http/method.py
StrEnum; _missing_ auto-extends, no failure path
No
pcapkit/const/ipv6/extension_header.py
no _missing_ at all
Yes — raised by aenum
pcapkit/const/tcp/flags.py
no _missing_ at all
No — and that is Part 2
pcapkit/const/http/method.py:182-193 is the first shape: _missing_ ends in return extend_enum(cls, name, value), so there is nothing to raise.
The second shape complicates the headline. pcapkit/const/ipv6/extension_header.py authors no raise, but aenum does it for them:
ExtensionHeader.get(99999) -> RAISED ValueError: 99999 is not a valid ExtensionHeader
is BaseError? False is bare builtin? True
So on observable behaviour 114 of 117 modules leak a bare ValueError, of which 113 are pcapkit's own text and one comes from the dependency and cannot be fixed by a template at all.
Part 2 — pcapkit/const/tcp/flags.py has no guard, and Flags(-1) returns every bit
class Flags(IntFlag) at pcapkit/const/tcp/flags.py:22 declares twelve members, bits 4 through 15 (Reserved_4 = 1 << 4 … FIN = 1 << 15). It defines no _missing_ — confirmed by execution, not by reading:
"_missing_" in vars(Flags): False
Flags._missing_ defined in : aenum._enum
so lookups fall to aenum's Flag machinery, which composes a pseudo-member for anything. Measured:
Flags(-1) is exactly the OR of every declared member — 65520 == 0xfff0, equal to the fold of all twelve — so a negative value silently reads as every TCP flag set at once. Flags(-65536) is worse in the other direction: it silently reads as no flags set. The TCP flags field is sixteen bits off the wire and can never be negative, so every one of these is an input the type should reject.
Contrast the same generated family, which does carry the guard (pcapkit/const/mh/binding_ack_flag.py):
"_missing_" in vars(BindingACKFlag): True
BindingACKFlag(-1) -> ValueError : -1 is not a valid BindingACKFlag
The consequence is not only the wrong value — it disables get's fallback
A caller that asked for 0 on failure gets all twelve flags instead, with no error and nothing logged. That is the default contract issue #584 established for the integer path, not holding here.
Why EnumError is the wrong fix as stated, which is the part worth reading
EnumError is genuinely unused. An unrestricted grep over the tree returns exactly three hits, and grep -rn "raise EnumError\|except EnumError" returns none:
file:line
Kind
pcapkit/utilities/exceptions.py:276
its own definition
pcapkit/utilities/exceptions.py:38
__all__ entry, under the # TypeError comment
docs/source/pcapkit/utilities/exceptions.rst:121
autodoc stub
But pcapkit/utilities/exceptions.py:276-277 is:
classEnumError(BaseError, TypeError):
"""The argument(s) must be *enumeration protocol* type."""
It derives from TypeError, not ValueError, and its docstring is about an argument's type rather than a value's range. issubclass(EnumError, ValueError) is False. Three measured consequences of swapping it into the template, from throwaway replica enums (no library file touched):
aenum does propagate it, so the raise itself works: Swapped(99999) -> EnumError: 99999 is not a valid Swapped.
tests/const/test_const_enum_get.py:287 — self.assertIn('except ValueError:', from_template.group(0)), which pins the template text directly and must change with it
By contrast test_the_vendor_templates_still_emit_the_fix (tests/const/test_const_enum_lookup.py:367-405) would not need updating: it renders each template and asserts assertEqual(rendered, committed), so it is self-consistent and passes as long as the template change is followed by a regeneration and commit. It usefully constrains the fix to "template and regenerate together" and will catch a template-only or tree-only edit — exactly the failure mode here.
Every discarded fallback would log CRITICAL and zero sys.tracebacklimit.BaseError.__init__ (pcapkit/utilities/exceptions.py:191-205) logs at construction, so it fires even when the exception is caught and thrown away — measured: 1 CRITICAL record and sys.tracebacklimit == 0 after a fallback that succeeded. The library's own answer is quiet=True (pcapkit/utilities/exceptions.py:165, used at pcapkit/corekit/multidict.py:183).
The repo's own precedent points the other way. tests/corekit/test_fields_ipaddress.py:454-464:
That inconsistency is deliberate and safe only because both classes derive from BaseError and from ValueError, so neither documented handler can tell them apart. […] used to raise a bareValueError, so they were catchable by except ValueError and not by except BaseError. They are now catchable by both -- a widening, not a break.
That is the established pattern: an in-library class deriving from BaseErrorand the builtin it replaces — ProtocolError(BaseError, ValueError) at :357, FieldValueError(BaseError, ValueError) at :373. Ten such ValueError-category exceptions exist at pcapkit/utilities/exceptions.py:349-385; EnumError is not among them.
So a correct fix is a coordinated change, not a string swap: either give the enum guard an exception deriving from ValueError as well as BaseError (the widening precedent above), or change the except ValueError in every template in lockstep — and in both cases pass quiet=True and update tests/const/test_const_enum_get.py:287, which pins the current template text.
One honesty note: no written rule in the tree says these guards must raise EnumError.docs/source/pcapkit/utilities/exceptions.rst documents the loud/quiet semantics and the category groupings, and CONTRIBUTING.md does not cover it. Part 1 is therefore a proposal grounded in the module's evident purpose, not a quotable convention being violated. Part 2 stands on its own regardless — a missing range guard is a defect under any exception policy.
The fix is a template change plus a regeneration, not a 113-file hand-edit
Everything under pcapkit/const/ is generated. The provenance line is in all 117 modules — pcapkit/const/arp/hardware.py:8-9:
This module contains the constant enumeration for **Hardware Types**,
which is automatically generated from :class:`pcapkit.vendor.arp.hardware.Hardware`.
and that banner is itself template text at pcapkit/vendor/default.py:60-61. There is no literal "do not edit" anywhere under pcapkit/const/; the generated-ness is established by the provenance line plus the regeneration tests.
The raise lives in 9 template files, not 1. There are 13 LINE templates under pcapkit/vendor/; these carry the canonical message:
plus the bare control-flow raise at pcapkit/vendor/reg/apptype.py:170. The canonical one, pcapkit/vendor/default.py:99-108:
@classmethoddef_missing_(cls, value: 'int') ->'{NAME}':
"""Lookup function used when value is not found. Args: value: Value to get enum item. """ifnot ({FLAG}):
raiseValueError('%r is not a valid %s'% (value, cls.__name__))
Nothing under const/ is hand-written, so regeneration does fix all 113 provided all 9 templates are edited: clustering every get() and _missing_ body by AST skeleton traces all 8 distinct get() shapes to a vendor template — 106 to default.py, and the bespoke ones (pcapkit/const/pcapng/option_type.py:201 taking namespace, pcapkit/const/reg/apptype.py:30585 taking proto, TransportProtocol at :41 with no default) to per-registry overrides that carry their own template text.
Part 2 is different in shape: tcp/flags.py's guard is absent, so the fix is to add one, in pcapkit/vendor/tcp/flags.py and then regenerate. Note the fix cannot simply copy the mh flag shape — issue #623 (fixed in #632) established that an overriding _missing_ on an IntFlag shadows the aenumFlag machinery that legitimately resolves composites of defined bits, which is precisely why Flags, defining no _missing_, never recursed. A guard for Flags has to reject out-of-range values while still delegating in-range composites to super()._missing_, as #632 landed for the four mh registries.
Regeneration routes, all evidenced, none run here:
make vendor — Makefile:69-70, pipenv run pcapkit-vendor
the pcapkit-vendor console script — pyproject.toml:107, backed by pcapkit/vendor/__main__.py
per-module python -m pcapkit.vendor.<pkg>.<module>, which is what the suite tells you to run (tests/vendor/test_ipx_socket_unit.py:230)
.github/workflows/cron-vendor.yml:59-67, weekly and on every push to main, which runs pcapkit-vendor then isort -l100 -ppcapkit pcapkit/const/*/*.py — note make vendor omits that isort pass, so it is not byte-identical to CI output
fix(mh): stop the four flag enums recursing on any non-member value (#623) #632 changed only the last line of those four _missing_ bodies (return cls(value) → return super()._missing_(value)); the raise ValueError guard above it is untouched, so Part 1's count is the same before and after it landed. Verified by reading origin/main directly.
fix(mh): stop the four flag enums recursing on any non-member value (#623) #632 also added ConstFlagMissingRecursionTests to tests/const/test_const_enum_lookup.py, whose test_the_range_guard_still_rejects (around :346) asserts assertRaises(ValueError) for obj(-1) over the four mh registries. Two things follow: a change to the exception type has to update that test too, and the same test's scope is the four mh registries rather than the full seven-class IntFlag sweep it sits beside — which is why Part 2's missing guard in Flags is not caught there.
The counts are of modules, not registries. A module may define several: pcapkit/const/ftp/command.py defines four. The repo's own class-level census is 118 (tests/const/test_const_enum_get.py:194). 117 modules and 118 registries are both right, about different things.
The generated
_missing_range guards underpcapkit/const/raise a bareValueError, in 113 of the 117 modules.pcapkit.utilities.exceptions.EnumErrorexists for exactly this and is entirely unused — three references in the whole tree, none of them araise. And one of the four modules that is exempt from the count is exempt for the worst possible reason:pcapkit/const/tcp/flags.pycarries no guard at all, soFlags(-1)returns every defined bit instead of rejecting a value no wire field can hold.Two defects in one census, filed together because the same sweep produces both and
tcp/flags.pyappears in each — as an exemption in the first and as the subject of the second.Measured on
375e9d411, CPython 3.14.7,aenum3.1.17, tree asserted against the worktree rather than the editable install (pcapkit.__file__ = …/pcapkit/__init__.pyprinted on every run). Every file named below is byte-identical on currentorigin/main(da381f259), verified withgit diff --stat 375e9d411..origin/main -- <path>, and the census was re-run against both trees and returns the same 113/117 in each — so nothing here is stale against the tree as it stands.Part 1 — 113 of 117 modules raise a bare
ValueErrorCounts derived here rather than taken on trust:
117 is 134
.pyfiles less 17__init__.py. All 117 carry the generated-provenance banner. An AST pass resolving every raise to its enclosing function gives:113 is the number to quote, not 114. The 114th,
pcapkit/const/reg/apptype.py:30605, is a bareraise ValueErrorused as internal control flow and caught two lines later, so it never escapes:The 113 real sites are byte-identical, one per module, all in
_missing_. Verbatim, with the guard above it —pcapkit/const/arp/hardware.py:180-181:Same line at
pcapkit/const/ipv4/option_number.py:143,pcapkit/const/mh/packet.py:125,pcapkit/const/reg/apptype.py:30623, and 109 others. The{FLAG}guard differs per registry — 90 distinct_missing_shapes — while the raise line does not vary at all.The four exempt modules, for two different reasons
ValueErrorstill reaches a caller?pcapkit/const/ftp/command.pyStrEnum;_missing_auto-extends, no failure pathpcapkit/const/http/method.pyStrEnum;_missing_auto-extends, no failure pathpcapkit/const/ipv6/extension_header.py_missing_at allaenumpcapkit/const/tcp/flags.py_missing_at allpcapkit/const/http/method.py:182-193is the first shape:_missing_ends inreturn extend_enum(cls, name, value), so there is nothing to raise.The second shape complicates the headline.
pcapkit/const/ipv6/extension_header.pyauthors no raise, butaenumdoes it for them:So on observable behaviour 114 of 117 modules leak a bare
ValueError, of which 113 are pcapkit's own text and one comes from the dependency and cannot be fixed by a template at all.Part 2 —
pcapkit/const/tcp/flags.pyhas no guard, andFlags(-1)returns every bitclass Flags(IntFlag)atpcapkit/const/tcp/flags.py:22declares twelve members, bits 4 through 15 (Reserved_4 = 1 << 4…FIN = 1 << 15). It defines no_missing_— confirmed by execution, not by reading:so lookups fall to
aenum'sFlagmachinery, which composes a pseudo-member for anything. Measured:Flags(-1)is exactly the OR of every declared member —65520==0xfff0, equal to the fold of all twelve — so a negative value silently reads as every TCP flag set at once.Flags(-65536)is worse in the other direction: it silently reads as no flags set. The TCP flags field is sixteen bits off the wire and can never be negative, so every one of these is an input the type should reject.Contrast the same generated family, which does carry the guard (
pcapkit/const/mh/binding_ack_flag.py):The consequence is not only the wrong value — it disables
get's fallbackpcapkit/const/tcp/flags.py:73-79:Because
Flags(key)never raises for these inputs, theexcept ValueErroris dead and the caller'sdefaultis silently ignored:A caller that asked for
0on failure gets all twelve flags instead, with no error and nothing logged. That is thedefaultcontract issue #584 established for the integer path, not holding here.Why
EnumErroris the wrong fix as stated, which is the part worth readingEnumErroris genuinely unused. An unrestricted grep over the tree returns exactly three hits, andgrep -rn "raise EnumError\|except EnumError"returns none:file:linepcapkit/utilities/exceptions.py:276pcapkit/utilities/exceptions.py:38__all__entry, under the# TypeErrorcommentdocs/source/pcapkit/utilities/exceptions.rst:121But
pcapkit/utilities/exceptions.py:276-277is:It derives from
TypeError, notValueError, and its docstring is about an argument's type rather than a value's range.issubclass(EnumError, ValueError)isFalse. Three measured consequences of swapping it into the template, from throwaway replica enums (no library file touched):aenumdoes propagate it, so the raise itself works:Swapped(99999) -> EnumError: 99999 is not a valid Swapped.The
get(key, default)fallback breaks, silently un-doing issue get()'s documented default is ignored on the integer path across the shared const/ enum template #584. The generatedget()catchesValueError(pcapkit/vendor/default.py:91, and ~113 generatedget()bodies, e.g.pcapkit/const/arp/hardware.py:164catching what:181raises). AnEnumErrorsails past it:That is pinned by the suite. The full blast radius on
origin/mainis 8assertRaises(ValueError)sites plus one template-text assertion:tests/const/test_const_enum_get.py:158,:175,:179,:219—:219loops the sweep and pinscovered == 110tests/const/test_const_enum_lookup.py:199,:358,:360,:362—:199sweeps 111 registries;:358-362are fix(mh): stop the four flag enums recursing on any non-member value (#623) #632's newtest_the_range_guard_still_rejects, 12 assertions over 4 enumstests/const/test_const_enum_get.py:287—self.assertIn('except ValueError:', from_template.group(0)), which pins the template text directly and must change with itBy contrast
test_the_vendor_templates_still_emit_the_fix(tests/const/test_const_enum_lookup.py:367-405) would not need updating: it renders each template and assertsassertEqual(rendered, committed), so it is self-consistent and passes as long as the template change is followed by a regeneration and commit. It usefully constrains the fix to "template and regenerate together" and will catch a template-only or tree-only edit — exactly the failure mode here.Every discarded fallback would log CRITICAL and zero
sys.tracebacklimit.BaseError.__init__(pcapkit/utilities/exceptions.py:191-205) logs at construction, so it fires even when the exception is caught and thrown away — measured: 1 CRITICAL record andsys.tracebacklimit == 0after a fallback that succeeded. The library's own answer isquiet=True(pcapkit/utilities/exceptions.py:165, used atpcapkit/corekit/multidict.py:183).The repo's own precedent points the other way.
tests/corekit/test_fields_ipaddress.py:454-464:That is the established pattern: an in-library class deriving from
BaseErrorand the builtin it replaces —ProtocolError(BaseError, ValueError)at:357,FieldValueError(BaseError, ValueError)at:373. Ten suchValueError-category exceptions exist atpcapkit/utilities/exceptions.py:349-385;EnumErroris not among them.So a correct fix is a coordinated change, not a string swap: either give the enum guard an exception deriving from
ValueErroras well asBaseError(the widening precedent above), or change theexcept ValueErrorin every template in lockstep — and in both cases passquiet=Trueand updatetests/const/test_const_enum_get.py:287, which pins the current template text.One honesty note: no written rule in the tree says these guards must raise
EnumError.docs/source/pcapkit/utilities/exceptions.rstdocuments the loud/quiet semantics and the category groupings, andCONTRIBUTING.mddoes not cover it. Part 1 is therefore a proposal grounded in the module's evident purpose, not a quotable convention being violated. Part 2 stands on its own regardless — a missing range guard is a defect under any exception policy.The fix is a template change plus a regeneration, not a 113-file hand-edit
Everything under
pcapkit/const/is generated. The provenance line is in all 117 modules —pcapkit/const/arp/hardware.py:8-9:and that banner is itself template text at
pcapkit/vendor/default.py:60-61. There is no literal "do not edit" anywhere underpcapkit/const/; the generated-ness is established by the provenance line plus the regeneration tests.The raise lives in 9 template files, not 1. There are 13
LINEtemplates underpcapkit/vendor/; these carry the canonical message:plus the bare control-flow raise at
pcapkit/vendor/reg/apptype.py:170. The canonical one,pcapkit/vendor/default.py:99-108:Nothing under
const/is hand-written, so regeneration does fix all 113 provided all 9 templates are edited: clustering everyget()and_missing_body by AST skeleton traces all 8 distinctget()shapes to a vendor template — 106 todefault.py, and the bespoke ones (pcapkit/const/pcapng/option_type.py:201takingnamespace,pcapkit/const/reg/apptype.py:30585takingproto,TransportProtocolat:41with nodefault) to per-registry overrides that carry their own template text.Part 2 is different in shape:
tcp/flags.py's guard is absent, so the fix is to add one, inpcapkit/vendor/tcp/flags.pyand then regenerate. Note the fix cannot simply copy themhflag shape — issue #623 (fixed in #632) established that an overriding_missing_on anIntFlagshadows theaenumFlagmachinery that legitimately resolves composites of defined bits, which is precisely whyFlags, defining no_missing_, never recursed. A guard forFlagshas to reject out-of-range values while still delegating in-range composites tosuper()._missing_, as #632 landed for the fourmhregistries.Regeneration routes, all evidenced, none run here:
make vendor—Makefile:69-70,pipenv run pcapkit-vendorpcapkit-vendorconsole script —pyproject.toml:107, backed bypcapkit/vendor/__main__.pypython -m pcapkit.vendor.<pkg>.<module>, which is what the suite tells you to run (tests/vendor/test_ipx_socket_unit.py:230).github/workflows/cron-vendor.yml:59-67, weekly and on every push tomain, which runspcapkit-vendorthenisort -l100 -ppcapkit pcapkit/const/*/*.py— notemake vendoromits thatisortpass, so it is not byte-identical to CI outputNotes
mhflag registries. Neither was in that change's scope, so they were reported rather than fixed. Filing them so they are not rediscovered at full cost._missing_bodies (return cls(value)→return super()._missing_(value)); theraise ValueErrorguard above it is untouched, so Part 1's count is the same before and after it landed. Verified by readingorigin/maindirectly.ConstFlagMissingRecursionTeststotests/const/test_const_enum_lookup.py, whosetest_the_range_guard_still_rejects(around:346) assertsassertRaises(ValueError)forobj(-1)over the fourmhregistries. Two things follow: a change to the exception type has to update that test too, and the same test's scope is the fourmhregistries rather than the full seven-classIntFlagsweep it sits beside — which is why Part 2's missing guard inFlagsis not caught there.pcapkit/const/ftp/command.pydefines four. The repo's own class-level census is 118 (tests/const/test_const_enum_get.py:194). 117 modules and 118 registries are both right, about different things.Flagsalso turns up in dumpkit's object_hook interpolates o.name unguarded, so a nameless flag member renders as Type::None [0] #648, from the other direction:pcapkit.const.tcp.flags.Flagsis the only registry in the whole ofpcapkit/const/that is nameless at zero, which is what makesFlags(0)render asFlags::None [0]there. The two are independent — dumpkit's object_hook interpolates o.name unguarded, so a nameless flag member renders as Type::None [0] #648 is the dumper's unguardedo.name, this is the registry's missing range guard — but both are about the same class and are worth reading together.get()integer path, fixed across 114 modules in fix(const): normalise the key get() looks up, and honour its documented default (#582, #583, #584) #596) and const enum lookups reject values that appear on the wire: RouterAlert(0) is RFC 2113's only defined value #492 (the registry-wide lookup sweep). This is the exception type and a missing guard, neither of which those touched.