Skip to content

feat(corekit): add EnumRegistry base for const registries, convert 5 (#775) - #855

Merged
JarryShaw merged 1 commit into
mainfrom
feat/775-tier2-enumregistry-base
Sep 27, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
feat/775-tier2-enumregistry-base

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • feat — adds a feature

Refs #775, #842 — tier 2, phase 1: the shared base class

Implements your ruling on #842 verbatim — "get/get_all/register/register_alias should always exist on the
const enums - so they're to be moved to the base class"
— as EnumRegistry in
pcapkit/corekit/enums.py, plus the first six registries converted onto it. Two review findings are
folded into this same commit rather than left for a follow-up: tcp/flags was originally scoped out on a
rationale that didn't hold, and register() had a silent-alias defect independent of the batch itself. Both
below.

This batch is deduplication, not defect reduction, and the numbers say so. AST tally by enclosing
function, main vs this branch, as measured for the original five-registry batch:

main    register 105   _missing_ 1013   get 13   register_alias 1   = 1132
branch  register 106   _missing_ 1013   get 13   register_alias 1   = 1133

The +1 is the single extend_enum inside EnumRegistry.register. The defect count is unchanged at 1026
(_missing_ 1013 + get 13) — these registries never minted. tcp/flags joining the batch continues the
same pattern with its own concrete numbers: grep -c 'def __new__' is 0 for both its vendor and const
modules (unchanged by this PR — it never had one), and its own hand-copied get is gone the same way the
other five's went. What goes away is 6 duplicated gets and 6 bespoke template copies; what arrives
is unchanged — 6 inherited methods with one call site between them.

Why corekit, not const

Two reasons, both structural. A hand-written module under pcapkit/const/** breaks the all-generated
invariant #838's census rests on; and importing pcapkit.const.<anything> from inside a const module runs
pcapkit/const/__init__.py's wildcard imports — a cycle. corekit is the reusable-core layer, already owns
the enum-adjacent EnumField, and imports nothing from const.

The three tiers you specified

Tier 1 is this class. Tier 2 (AppType's sub-base with the dispatching) and tier 3 (the transport
subclasses) are not built
— AppType keeps its own methods, untouched. The module docstring names both and
why.

A design objection that measurement overturned

The worker first built this as a generated template fragment in pcapkit/vendor/default.py, arguing a mixin
might not survive aenum treating a non-Enum base as the member data type. It then measured, found the
objection false, and reverted vendor/default.py entirely — it is not in this commit. Confirmed here:

HandoverACKFlag   bases=[EnumRegistry, IntFlag]   _member_type_=int   EnumRegistry in MRO=True
ExtensionHeader   bases=[EnumRegistry, IntEnum]   _member_type_=int   EnumRegistry in MRO=True

So one base serves IntEnum, IntFlag and StrEnum — which the fragment could not, since it hardcoded
int.__new__.

Why these six, and the blocker for the next batch

mh/{binding_ack,binding_update,handover_ack,handover_initiate}_flag, ipv6/extension_header, and
tcp/flags are the only bespoke-template registries with no custom __new__, so the shared
_unregistered_member — which sets only _name_/_value_ — builds a complete member.

tcp/flags was left out on a rationale a review measurement disproved. The first draft grouped it with
the five below on the claim all six "each attach extra attributes in __new__" — wrong for tcp/flags:

tcp/flags              vendor:0 const:0      <- no __new__ anywhere
ftp/command, ftp/return_code, http/method, http/status_code, pcapng/option_type   vendor:1 const:1 each

Its vendor template had only a get staticmethod and a range-checking _missing_ — the same shape as the
four mh/*_flag modules already converted. So it joins them: EnumRegistry mixed in, get removed,
_missing_ untouched — same range check, same super()._missing_(value) tail, which still resolves
through aenum's own Flag machinery since EnumRegistry never defines _missing_ itself. Pinned by
TCPFlagsConversionTests.test_missing_resolves_identically_to_the_pre_conversion_shape, sweeping every
member, boundary and several composites against a byte-for-byte pre-conversion reproduction.

The remaining five — ftp/command, ftp/return_code, http/method, http/status_code,
pcapng/option_type — do each attach extra attributes in __new__ (description/kind/group,
message, safe/idempotent), so an unregistered member of theirs would be missing them. Unchanged, and
still the blocker for the next batch.

Review finding: register() silently aliased instead of minting

register() was a bare extend_enum(cls, name, value). aenum treats an already-registered value as an
alias request: register(existing_value, 'TOTALLY_NEW_NAME') returned the existing member unchanged, made
'TOTALLY_NEW_NAME' reachable in __members__ pointing at it, and minted nothing — no exception,
contradicting the docstring's own "mints ... so we don't have to guess blindly" and a Raises: section
naming only the name-collision case. register_alias() already guarded the inverse case.

Fix: register() now checks value in cls._value2member_map_ (same table, same reason
register_alias already checks it) and raises ValueError naming the existing member and pointing at
register_alias(). Since register_alias() depends on that value already existing, the two no longer
call each other — both route through a new shared, ungated _extend(), so register_alias's
mint-or-alias mechanism stays intact while register() refuses it.

Checked every .register( call site (git grep -n '\.register('): none call EnumRegistry.register with
an already-registered value, and the unrelated ones — ContextRegistry.register, Protocol.register,
Schema.register (Frame.register, TCP.register, Option.register, ...) — share none of
EnumRegistry's contract, so they're out of scope, not residue.

The guard does not reach the 105 generated const-enum registries, and that is residue worth naming.
Measured: def register( appears in 105 files under pcapkit/const (excluding __init__.py) — the
tier-1 default-template census from #838. pcapkit/vendor/default.py is untouched by this PR, so each still
emits the identical bare extend_enum(cls, name, value), the same signature
(cls, value: 'int', name: 'str'), even the same docstring sentence — "the caller-named entry point that
still grows the registry". Measured on this head: TransType.register(6, 'TOTALLY_NEW_NAME') (6 is TCP)
returns TransType.TCP unchanged, mints nothing, no exception — unchanged by this PR. So pcapkit.const
now ships two contradictory register contracts: Flags.register(existing, 'X') raises,
TransType.register(existing, 'X') silently aliases; before this PR every registry that had a register()
at all shared the same ungated one. Deliberately not widened here — the fix belongs in
pcapkit/vendor/default.py, in a later tier of #775.

Not fully closed either way: a bitwise-composite IntFlag value never before resolved still mints
under the caller's name on register() — aenum's own flag-composition semantics, already pinned by
test_register_on_a_flag_registry_resolves_by_name_and_value, and narrower than it sounds: once any lookup
resolves the composite once, it's in _value2member_map_ and the new guard catches it same as any other
value. See UNVERIFIED.

get() also changed observable behaviour, not previously disclosed

Measured on both trees (pcapkit.__file__ asserted per run):

                        origin/main (ad0ab97e4)          head (cb7eb0d94)
Flags.get('NOPE', 0)    KeyError: 'NOPE'                  0
Flags.get(3.5)          TypeError: ... not a container   ValueError: 3.5 is not a valid Flags

(Full main-side message: TypeError: argument of type 'float' is not a container or iterable.)

The first is a widening, and a fix, not a regression. The generated get (pcapkit/vendor/default.py)
already catches KeyError on its string path and falls back to default; the bespoke templates' hand-copied
get never did, ending in a bare return {NAME}[key]. The base aligns the bespoke registries with the rest
of the tree. This applies equally to all five registries converted in round 1, confirmed against
ad0ab97e4: ExtensionHeader.get('NOPE', 0) and BindingACKFlag.get('NOPE', 0) both raised the same
uncaught KeyError pre-conversion — undisclosed then, disclosed now.

The second is a new divergence. The base dispatches on isinstance(key, str); the generated get
dispatches on isinstance(key, int). A key that is neither is a value to the base (tries cls(key),
raises ValueError) and a name to the generated one (tries cls[key], raises TypeError). Degenerate
today — no caller passes a float — but it will matter once the remaining 105 migrate with their own callers
unchanged.

Verification

  • Byte-reproducible across two consecutive live crawls, with a control: const/arp/hardware.py stayed at
    0e195aeacb4caececc210ecf040abae9, so no IANA churn rode along.
  • tests/const + tests/vendor: 220 passed, 39780 subtests passed; tests/const/test_const_registry_protocol.py
    alone: 31 passed / 133 subtests, re-derived as Ran 31 tests … OK under plain unittest.
  • Failing-before, both findings, on this PR's prior head 02296b5dd: the six new/extended assertions
    covering tcp/flags joining CONVERTED (test_tcp_flags_now_inherits_the_base,
    test_tcp_flags_gains_the_protocol_it_never_had, plus the generic sweeps now covering Flags) fail with
    AttributeError/AssertionError there, and the two new register()-guard tests
    (test_register_over_a_taken_value_raises_value_error_and_does_not_alias,
    test_register_over_a_taken_value_on_a_flag_registry_also_refuses) fail with ValueError not raised.
    17 assertions fail on 02296b5dd across the two touched test files; all pass on this head.
  • pcapkit/corekit/enums.py retains 100% line and branch coverage. pylint 10.00/10, mypy clean, isort
    clean.
  • Two pre-existing tests updated, faithfully, for the class-statement/method-order change:
    test_const_enum_lookup.py's render regex (already updated for the original five) and
    test_const_enum_builtin_parity.py's test_the_tcp_flags_template_renders_the_committed_module, which
    hardcoded class \w+\(IntFlag\) and @staticmethod, both of which legitimately changed shape for
    tcp/flags too.

Contract notes worth your eye

UNVERIFIED

  • register on an IntFlag whose value is a bitwise composite that has never been resolved before
    still registers an aenum pseudo-member rather than growing _member_names_. Name and value still
    resolve; whether that is acceptable for flag registries generally is not established and may want a
    register override there. Narrower than it first looked: once a value has been resolved once (by any
    lookup), it lands in _value2member_map_ and the new guard catches a second register() on it same as
    any other already-registered value.
  • register_aliases is not transactional — names before a failing one stay registered.
  • Only tests/const and tests/vendor were run, per the OOM constraint. tests/protocols and
    tests/toolkit are unverified against the new pcapkit.corekit import in six const modules; CI is the
    first full check. (Targeted spot checks were run this round against tests/protocols/transport/test_tcp_udp_unit.py,
    the two MPTCP flag-ordering/error-message files, and tests/dumpkit/test_nameless_enum_rendering_unit.py,
    all of which exercise pcapkit.const.tcp.flags.Flags directly and pass.)
  • Sphinx rendering of the now-inherited methods is unverified — the old generated get carried
    :meta private:, so the base's get may surface in docs where it previously did not.
  • get's string branch only tries the name table (cls._member_map_[key]) and never falls through to
    cls(key), so on a StrEnum registry a valid value that isn't also a member's name raises KeyError
    rather than resolving — confirmed on an inline EnumRegistry, StrEnum fixture: .get('known_value') (the
    value) raises KeyError, .get('known') (the name) resolves. Harmless today, since no committed
    StrEnum registry uses this base yet, but the docstring's "given a value it is cls(key)" is untested for
    that member type; test_str_valued_registries exercises only _unregistered_member.

@JarryShaw JarryShaw added feat Pull requests that add a new capability (feat: subject prefix) const Regenerated IANA or vendor constant tables; members keep their numeric values refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix) review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on 02296b5dd — cross-review (opus, a different model from the sonnet author). Two defects,
both found by construction rather than by re-reading the PR's own claims, and I reproduced both myself before
relaying
.

1. The stated blocker for tcp/flags is factually wrong. The PR says the six excluded bespoke templates
"each attach extra attributes in __new__". Five do. tcp/flags does not — measured across all six, on both
sides:

tcp/flags              vendor:0 const:0      <- no __new__ anywhere
ftp/command            vendor:1 const:1
ftp/return_code        vendor:1 const:1
http/method            vendor:1 const:1
http/status_code       vendor:1 const:1
pcapng/option_type     vendor:1 const:1

pcapkit/vendor/tcp/flags.py:59-95 has only a get staticmethod and a range-checking _missing_ — the exact
shape of the four mh/*_flag modules this PR does convert. Building an unregistered member through the new
base works cleanly on it, no AttributeError, registry untouched. So tcp/flags meets this batch's own
criterion and was excluded on a false premise. Either convert it here, or correct the rationale — it is
what the next batch's scope will be read off.

2. register() silently aliases instead of minting when the value already has a member, and it is
untested.
register() is a bare extend_enum(cls, name, value); aenum treats an existing value as an alias
request. Verified on TransType (value 6 is already TCP), asserted against the PR tree:

T.register(6, 'TOTALLY_NEW_NAME')  ->  <TransType.TCP: 6>   .name = 'TCP'   is T.TCP -> True
'TOTALLY_NEW_NAME' in T.__members__ -> True     # reachable, under the WRONG .name
set(T._member_names_) - before       -> set()   # nothing minted

No exception. That contradicts the method's own contract — "register mints new enum … so we don't have to
guess blindly"
— and the docstring's Raises: documents only the name-collision case. register_alias()
guards the inverse (value not in cls._value2member_map_ → ValueError); register() has no matching guard.
Add the guard, or document and test the silent-alias fallback. The new test file covers register() on an
unused value and on an IntFlag composite, but not this.

Everything else confirmed, some of it more tightly than the PR claimed:

  • get, get_all, register_alias, register_aliases, _unregistered_member all match your const: generalise register_alias from AppType to every pcapkit.const enum #842 contract
    on direct testing — alias-name lookup through get resolves to the canonical object, get_all returns a
    1-tuple for a one-to-one registry, register_alias refuses a ghost value.
  • _unregistered_member verified across IntEnum, IntFlag and a hand-built class Probe(EnumRegistry, StrEnum) (none ships in this batch): _member_type_ resolves correctly, the minted member is absent from
    _member_map_/_value2member_map_/__members__, and isinstance holds against both the concrete class and
    the mixin type.
  • Both previously-unverified items now verified as disclosed: an IntFlag composite whose pseudo-member was
    already computed in-process returns aenum's cached member named 'A|H' rather than the requested name
    (order-dependent, no current call site exercises it); register_aliases is non-transactional exactly as
    stated.
  • tests/protocols/internet/test_mh_unit (50), test_ipv6_unit (10), test_ipv6_extension_unit (61) all pass
    against the new import, and the five converted const modules plus all five pcapkit.toolkit.* import
    together with no cycle.
  • test_const_enum_lookup.py's regex update is faithful, not weakened.
  • Labels feat/refactor/const with no breaking are correct; the diff touches exactly the 14 files
    described.

One caveat on your open question about exception types: the cited precedent
(pcapkit/corekit/fields/numbers.py:600-618) justifies a bare ValueError because the same generated
module's own
get() catches it — a self-contained internal signal. register()/register_alias()'s
ValueError is caller-facing with nothing catching it, so the citation is a weaker fit than the PR presents.
get/get_all are on firmer ground, mirroring stdlib Enum.__getitem__/__call__.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 27, 2026
…#775)

Tier 2 of #775, the abstraction half. Per the ruling on #842 --
"get/get_all/register/register_alias should always exist on the const
enums - so they're to be moved to the base class" -- the four methods now
live once, on `pcapkit.corekit.enums.EnumRegistry`, instead of being
written out as generated text in `pcapkit/vendor/default.py`'s template
and hand-copied into each of the eleven crawlers that override it.

- Add `EnumRegistry`, a plain mix-in carrying `get`, `get_all`,
  `register`, `register_alias`, `register_aliases` and
  `_unregistered_member`, with each method's contract as ruled.
- `register` now refuses a value that already has a member instead of
  silently aliasing it via `aenum.extend_enum` under the caller's name --
  the inverse of the check `register_alias` already ran on `value`.
  Both now route through a new shared `_extend`, so `register_alias`
  keeps the mint-or-alias behaviour it depends on.
- Convert six bespoke templates onto the base -- the four `mh/*_flag`,
  `ipv6/extension_header`, and `tcp/flags` -- and regenerate. `tcp/flags`
  was scoped out on the claim that it "attaches extra attributes in
  `__new__`"; measured, it defines none, the same shape as the four
  `mh/*_flag` templates, so it joins them instead of the five that do.
- `get`'s string miss now honours `default` on those six, where their
  own copy ended in a bare `return NAME[key]`.
- Update the template-render regexes in `test_const_enum_lookup.py` and
  `test_const_enum_builtin_parity.py` for the new class statement.

No behaviour change for the other 115 registries, nor for `tcp/flags`'s
own range-checking `_missing_`, which still ends in
`super()._missing_(value)` unchanged. `tests/const` and `tests/vendor`
pass.
@JarryShaw
JarryShaw force-pushed the feat/775-tier2-enumregistry-base branch from 02296b5 to cb7eb0d Compare September 27, 2026 15:52
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES on cb7eb0d94 — second cross-review (opus). The code is correct. Both required changes are
to the PR body: one claim measurement disproves, one undisclosed behaviour change. Round 1 failed on exactly this
class of defect, so the same standard applies.

1. The call-site claim is wrong, and it hides a repo-wide inconsistency this PR introduces. The body says the
.register( sweep found only "unrelated register methods on other classes — ContextRegistry.register,
Protocol.register, Schema.register… none sharing EnumRegistry's contract."
It omits the generated
const-enum register classmethods
, which share the contract exactly — same signature (cls, value: 'int', name: 'str') and the same docstring sentence, "the caller-named entry point that still grows the registry", emitted
from pcapkit/vendor/default.py, which this PR does not touch. They are still the bare ungated extend_enum:

TransType.register(6, 'TOTALLY_NEW_NAME')   # 6 is TCP
  -> <TransType.TCP: 6>   is TransType.TCP -> True
  'TOTALLY_NEW_NAME' in __members__ -> True    _member_names_ delta -> set()

So after this PR pcapkit.const ships two contradictory register contracts: Flags.register(existing, 'X')
raises ValueError; TransType.register(existing, 'X') silently aliases. Before it, all behaved alike. Scoping
the guard to this tier is defensible; asserting a clean sweep that implies no residue is not. State the residue
and the plan for vendor/default.py.

One correction to the review: it put the residue at 115. I counted 105 — files under pcapkit/const
(excluding __init__) carrying def register( on this head. That matches the 105 established by #838's own
census. Use 105.

2. An undisclosed observable change in get. Measured by me on both trees, pcapkit.__file__ asserted:

                    origin/main (ad0ab97e4)                     head (cb7eb0d94)
Flags.get('NOPE', 0)   KeyError: 'NOPE'                          <Flags: 0>
Flags.get(3.5)         TypeError: ... not a container            ValueError: 3.5 is not a valid Flags

The first is a widening and a fix — vendor/default.py shows the generated gets already honoured default
on the KeyError path, so the base aligns the bespoke registries with the rest. It applies equally to the five
converted in round 1 and went unreported then too. The second is a new divergence: the base dispatches on
isinstance(key, str) where the generated get dispatches on isinstance(key, int), so a non-str/non-int key is
a value to one and a name to the other. Degenerate today; it will matter when the rest migrate. Both belong in
the body.

What the review confirmed, including by refuting my own premise. I briefed it that tcp/flags's range-checking
_missing_ was the risk — whether the base's _unregistered_member preserved it. It does not use
_unregistered_member at all.
_missing_ is byte-identical between trees and ends in super()._missing_(value);
EnumRegistry defines no _missing_, so the next _missing_ in the MRO is Flag on both trees. Nothing
widened because nothing moved. Probed per tree with values snapshotted before any lookup: in-range, out-of-range,
both boundaries, composites, member names and table growth (v2m 12→17 from aenum caching, identical both sides)
— no observable difference on any value lookup. _member_type_ still int, mixin ahead of IntFlag.

Also confirmed: the guard raises ValueError, matching register_alias's inverse guard, and is documented in
Raises:; register/register_alias now both route through a new ungated _extend, which is what keeps aliasing
working — a naive guard would have broken every register_alias call, and the author caught that. Two new
tests fail on 02296b5dd with ValueError not raised and pass here. The census is right: six templates with
__new__:0 all converted, the five with __new__:1 excluded. Neither touched test was weakened — both regexes are
strictly more specific, and assertIsNotNone still fails on a non-match. tests/const 134 OK, tests/vendor 86
OK under plain unittest.

breaking is not required — nothing renamed, no value changed, no member lost, composite and range behaviour
bit-identical. The one widening converts a raise into a return, so no working caller breaks. feat, refactor,
const stand.

One note for tier 2, no action now: the base get never tries the value path for a str key, so on a StrEnum
registry a valid value that is not a name would raise KeyError. Harmless today — all six converted are
IntEnum/IntFlag and all four StrEnum registries are excluded — but test_str_valued_registries exercises only
_unregistered_member, so the docstring's "given a value it is cls(key)" is untested for str-valued registries.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO on cb7eb0d94. Both required changes are done as body edits with no code change, so round 2's
confirmation that the code is correct carries forward to this exact sha.

Verified by me on the current body:

  • The residue is disclosed at the "does not reach the 105 generated const-enum registries" paragraph, with
    pcapkit/vendor/default.py named as the later-tier fix. It uses 105, matching my own count and fix(vendor): stop get() minting; _missing_ fixed for 21 of 105 (#775) #838's
    census. The separate "other 115 registries migrate with no signature change" line is a different quantity —
    121 const modules minus the 6 converted — and is not in conflict.
  • Both get changes are documented: the string-miss widening (correctly framed as a fix, since the generated
    get already caught KeyError on its string path while the hand-copied bespoke ones did not), and the
    isinstance(key, str) vs isinstance(key, int) dispatch divergence, flagged as degenerate today but real once
    the rest migrate. The worker also established that the same widening applied pre-conversion to
    ExtensionHeader.get and BindingACKFlag.get — round 1's own undisclosed change, now on the record.
  • The StrEnum note I raised for a later tier was checked rather than taken on faith: on an inline
    EnumRegistry, StrEnum fixture, .get('<a known value>') raises KeyError while .get('<the name>')
    resolves, so get's string branch genuinely never falls through to the value path. It is in UNVERIFIED where it
    belongs.

What the two rounds cost, for the record. Round 1 caught a factually wrong exclusion rationale — tcp/flags
has no __new__ at all (0/0 against 1/1 for the other five), so it met the batch's own criterion and was
excluded on a premise that did not apply — and a register() that silently aliased instead of minting. Round 2
confirmed the fixes and caught the two body defects above. My own briefed premise was refuted in round 2 and it
is the more useful finding: I said the risk was whether the base's _unregistered_member preserved TCPFlags's
range-checking _missing_. It does not use _unregistered_member at all — _missing_ is byte-identical between
trees and EnumRegistry defines none, so the next in the MRO is Flag on both trees. Nothing widened because
nothing moved, which is a stronger result than "preserved".

breaking correctly absent, re-judged with TCPFlags in scope: nothing renamed, no value changed, no member
lost, and composite, boundary and range behaviour bit-identical across in-range, out-of-range, both boundaries and
composites, with v2m growth 12→17 from aenum caching identical on both sides. The one widening turns a raise into
a return, so no working caller breaks.

Two cross-review rounds on opus against a sonnet author. tests/const 134 and tests/vendor 86 both OK under
plain unittest; 372 passed / 40797 subtests across those plus test_tier_guard.py and
tests/protocols/internet/test_mh_unit.py. Ready for you to merge once CI finishes — and note it is BEHIND
current main under the strict policy, so it may need updating before the button is available.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

I'm wondering if we should change the NO_DEFAULT to a sentinel object. Not necessarily this PR. Lmk if you wanna do it here or postpone and I merge as is.

@JarryShaw

Copy link
Copy Markdown
Owner Author

I'm wondering if we should change the NO_DEFAULT to a sentinel object. Not necessarily this PR. Lmk if you wanna
do it here or postpone and I merge as is.

Postpone — merge as is. The recommendation is measured, not a preference:

NO_DEFAULT consumers on this head : 1 file, 5 lines   (pcapkit/corekit/enums.py:64,103,123,140,146)
generated registries still on -1  : 108 files, their own `default: 'int' = -1`
minimum value across all const registries : 0     <- -1 is unreachable as a real value today

So there is no correctness bug to fix now — -1 cannot collide with a legitimate value, because nothing in
pcapkit/const goes below 0. It is a clarity improvement, not a defect.

And doing it here would make things worse in the one way this PR already got called out for. Round 2's finding was
that #855 leaves pcapkit.const with two contradictory register contracts — the base guards, the 108
generated ones still silently alias. Switching the base to a sentinel object while those 108 keep -1 adds a
second such split, on get this time. The fix that actually closes both is the tier that rewrites
pcapkit/vendor/default.py and regenerates, where base and generated change atomically. I have added it to #775 as
a scoped item rather than opening a separate issue.

One wart worth knowing while it stands, since it is the argument for the sentinel and I would rather state it than
have it discovered later: the check is default == NO_DEFAULT, and -1.0 == -1 is True, so get(key, -1.0)
silently means "no default" instead of defaulting to -1.0. Degenerate — no caller passes a float — but it is
exactly the class of thing a sentinel object makes impossible.

When it happens, object() is the right shape rather than a subclass or an enum member: it cannot compare equal to
anything a caller might pass, and it needs no import at the call sites since default keeps its default value.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Okay let's save the sentinel change to an issue (existing or new).

@JarryShaw

Copy link
Copy Markdown
Owner Author

Recorded, verbatim: "Okay let's save the sentinel change to an issue (existing or new)."

Filed as #857 — const: make EnumRegistry.get's NO_DEFAULT a sentinel object, not -1. Labelled
enhancement, const, blocked, with a checkable blocker written on it: the tier of #775 that rewrites
pcapkit/vendor/default.py, since the base and the 108 generated default: 'int' = -1 signatures have to change
together. Also cross-linked from #775's scoped item.

A dedicated issue rather than only the #775 comment, because #775 is an umbrella that stays open across several more
tiers, and a scoped item in its comment thread lands only if whoever does that tier reads back through it.

Nothing further on this PR — #855 is review: good-to-go at 58✅/0❌/0 inc and yours to merge.

@JarryShaw
JarryShaw merged commit 05468a0 into main Sep 27, 2026
62 checks passed
@JarryShaw
JarryShaw deleted the feat/775-tier2-enumregistry-base branch September 27, 2026 16:37
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Sep 27, 2026
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…numRegistry (#775)

Tier 3 of #775/#842: mixes pcapkit.corekit.enum.EnumRegistry into the
generated const-enum template and every registry it produces, so get,
get_all, register and register_alias exist on all 121 const registries
rather than 7. Closes the register() two-contract residue #855 disclosed:
an already-registered value now raises instead of silently aliasing.

- pcapkit/vendor/default.py: LINE now emits
  class {NAME}(EnumRegistry, IntEnum) and drops the hand-written get,
  register and _unregistered_member the base now provides; the aenum
  import line only keeps extend_enum when a registry's own _missing_
  still calls it directly. _missing_ itself is untouched.
- 105 generated pcapkit/const/*.py files edited to match, mechanically
  (script-derived from the template's own literal text). Verified
  byte-identical against a live regeneration of 5 files spanning both
  import shapes (arp/hardware, sctp/cause_code, hip/parameter,
  pcapng/record_type, reg/ethertype).
- Excluded: the 6 bespoke registries #855 already converted, and 10
  files with bespoke, mint-on-lookup get()/_missing_ contracts that
  don't share this template (ftp/command, ftp/return_code, http/method,
  http/status_code, pcapng/option_type, reg/apptype/* - AppType is
  tier 2 of #842, not tier 3). All are StrEnum-based; measured that the
  base get()'s str-key path never tries the value path, so converting
  them would silently regress value lookups.
- Measured the one real dispatch difference: a key that is neither int
  nor str now raises ValueError (treated as a value) instead of the old
  KeyError (treated as a name). Int and str keys are unaffected.
- Per owner ruling on #858: renamed pcapkit/corekit/enums.py to
  pcapkit/corekit/enum.py (git mv, no content change beyond the two
  self-referencing docstring lines naming the module's own path) and
  updated all 124 importers, corekit/__init__.py's re-export, and the
  four test modules that name the path in prose or assertions. No
  docs/ reference existed to update.
- tests/const/test_const_registry_protocol.py: new coverage for the
  105-file batch - base declared/inherited, full-corpus sweep, the
  register() regression pin (fails on prior head, passes here),
  _missing_ range parity, the get() dispatch matrix, and the StrEnum
  exclusion's own measurement.
- tests/const/test_const_enum_get.py: its vendor-template regression
  test asserted on the now-removed get() block; rewritten to assert the
  mix-in and the absence of a local copy instead.

Build: mypy/isort/pylint clean on all touched files (pylint duplicate-code
findings dropped 108->4 on this batch). tests/const, tests/vendor and
tests/test_tier_guard.py pass in full (352 tests, plain unittest).
@JarryShaw JarryShaw added the breaking Breaks public-facing behaviour or API (apply alongside the type label) label Sep 27, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

breaking backfilled onto this merged PR, for consistency with #858 rather than as a new finding.

#855 gave EnumRegistry.register() its guard for 6 classes; #858 extends the identical semantic to 105.
Measured on one of them, the pre-guard behaviour was not merely "returns the existing member" — it installed the
alias
:

before  TransType.register(6, 'TOTALLY_NEW_NAME') -> <TransType.TCP: 6>   'TOTALLY_NEW_NAME' in __members__: True
after   ValueError: 6 is already registered on TransType as 'TCP'; use TransType.register_alias() ...

register is a documented public classmethod, so a downstream consumer using it as an aliasing shortcut breaks on
upgrade — which is this repo's settled breaking test (could correct, working caller code break?) answered yes
for an external caller. #858's cross-review raised the inconsistency and I agree with it: label both, rather than
labelling only the larger one.

No code implication, and nothing to re-review — this is a label correction on merged work.

JarryShaw added a commit that referenced this pull request Sep 27, 2026
EnumRegistry.register() now raises ValueError naming the existing member
and pointing at register_alias(), where it used to silently alias an
already-registered value under the caller's new name -- aenum treats a
taken value as an alias request, so the old bare extend_enum() minted
nothing and raised nothing, contradicting the method's own docstring.

pcapkit.corekit.enums.EnumRegistry centralises get/get_all/register/
register_alias/register_aliases/_unregistered_member in one mixin and
converts the first six registries with no bespoke __new__. The new guard
reaches only those six; the other 105 const registries still carry the
old generator's ungated extend_enum, so pcapkit.const ships two
contradictory register contracts until #858 migrates the rest -- residue
the PR names rather than papers over.

Verified independently rather than taken from the PR table: the base's
get() dispatch divergence on a non-int/non-str key raises KeyError on the
still-unconverted generated template (empirically confirmed against
05468a0, not the TypeError the PR body itself claims for that path).
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…rated template

pcapkit/vendor/default.py's generated template now emits
class {NAME}(EnumRegistry, IntEnum) and drops its own get/register/
_unregistered_member entirely, so every const-enum class it produces
inherits #855's guard instead -- e.g. TransType.register(6,
'TOTALLY_NEW_NAME') now raises ValueError naming the existing member and
pointing at register_alias(), where it used to mint nothing and raise
nothing.

Census re-derived independently by AST over the merge commit rather than
taken from the PR table: 121 const modules hold 127 enum classes (three
modules define more than one class each). 6 classes already used
EnumRegistry from #855; of the other 115 modules, 105 share the generated
template byte-for-byte and are converted here, and the remaining 10 keep
their own bespoke __new__ and are left alone -- ftp/command (4 classes),
ftp/return_code (3), http/method, http/status_code, pcapng/option_type,
reg/apptype/apptype.py (2) and its four transport subclasses. Total now
inheriting EnumRegistry: 111 of 127 classes, up from 6. Also renames
pcapkit/corekit/enums.py to enum.py (no -s), the maintainer's ruling.

util/changelog_md.py regenerated CHANGELOG.md for all three entries added
across this and the two preceding commits (#855, #856, #858); --check
exit 0.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…- it was wrong

14c8930's commit message states: "the base's get() dispatch divergence
on a non-int/non-str key raises KeyError on the still-unconverted
generated template (empirically confirmed against 05468a0, not the
TypeError the PR body itself claims for that path)." That claim is false.
The test behind it called TransType.get(3.5) -- TransType is a generated
registry #855 never touched, so it only ever tells you about #858's
population, not #855's.

Re-tested properly this round: detached worktrees at 895cde6 (856,
immediately before 855) and 05468a0 (855), editable finder evicted from
sys.meta_path, pcapkit.__file__ asserted per process, one throwaway
process per cell since these registries mint on a fresh value.

  Flags.get(3.5) at 895cde6 (855's own six, pre-conversion):
    TypeError: argument of type 'float' is not a container or iterable
  Flags.get(3.5) at 05468a0 (855's own six, post-conversion):
    ValueError: 3.5 is not a valid Flags
  TransType.get(3.5) at both refs (generated, #855 never touches it):
    KeyError: 3.5 -- unchanged by #855, this is #858's population

So #855's PR body was right: TypeError is what the six bespoke registries
raised before this PR. KeyError belongs to the still-unconverted 105 and
is correctly attributed to #858's entry, not #855's. The #855 entry is
corrected back to TypeError, scoped to its own six registries' own
before/after rather than compared against "the generated template ...
used by the other 105" -- that comparison belongs to #858, which already
states it correctly.

14c8930 is left as-is rather than amended: it has already been pushed,
and rewriting it would need a force-push, which this branch does not do.
This commit is the correction of record instead.

util/changelog_md.py regenerated CHANGELOG.md; --check exit 0.
JarryShaw added a commit that referenced this pull request Sep 27, 2026
…s tense

Two fixes, both cross-review findings on #657.

1. The #855 entry's TypeError claim was generalised from one registry
(Flags) to all six #855 converts. The split is on member type, not on
converted-vs-not: the pre-855 hand-copied get() ends `return cls[key]`,
which aenum treats as a containment test on a Flag subclass (TypeError)
but a name lookup on a plain Enum (KeyError). Re-derived per-registry on
895cde6 (immediately before #855), MRO printed per cell:

  ExtensionHeader  [IntEnum, int]   get(3.5) -> KeyError: 3.5
  BindingACKFlag   [IntFlag, int]   get(3.5) -> TypeError: argument of
                                     type 'float' is not a container or
                                     iterable
  Flags            [IntFlag, int]   get(3.5) -> TypeError: ... (as above)

Confirmed post-855 (05468a0) that ExtensionHeader.get(3.5) now raises
ValueError, the same KeyError->ValueError shift #858's entry already
attributes to the other 105 -- so for ExtensionHeader specifically there
is no new divergence at all, only the same shift arriving six registries
early. Scoped the TypeError claim to the five IntFlag registries and
named ExtensionHeader's own KeyError->ValueError shift separately.

2. The #584 entry said -1 "was later replaced" by a sentinel object --
completed past tense for something that has not landed. gh pr view 859
-> OPEN, mergedAt null; origin/main:pcapkit/corekit/enum.py:64 still
reads NO_DEFAULT = -1. Reworded to present-continuous with an explicit
"not yet landed", so the entry does not tell a reader 1.5.0 ships
something it does not yet ship.

util/changelog_md.py regenerated CHANGELOG.md for both fixes.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
EnumRegistry.register() now raises ValueError naming the existing member
and pointing at register_alias(), where it used to silently alias an
already-registered value under the caller's new name -- aenum treats a
taken value as an alias request, so the old bare extend_enum() minted
nothing and raised nothing, contradicting the method's own docstring.

pcapkit.corekit.enums.EnumRegistry centralises get/get_all/register/
register_alias/register_aliases/_unregistered_member in one mixin and
converts the first six registries with no bespoke __new__. The new guard
reaches only those six; the other 105 const registries still carry the
old generator's ungated extend_enum, so pcapkit.const ships two
contradictory register contracts until #858 migrates the rest -- residue
the PR names rather than papers over.

Verified independently rather than taken from the PR table: the base's
get() dispatch divergence on a non-int/non-str key raises KeyError on the
still-unconverted generated template (empirically confirmed against
05468a0, not the TypeError the PR body itself claims for that path).
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…rated template

pcapkit/vendor/default.py's generated template now emits
class {NAME}(EnumRegistry, IntEnum) and drops its own get/register/
_unregistered_member entirely, so every const-enum class it produces
inherits #855's guard instead -- e.g. TransType.register(6,
'TOTALLY_NEW_NAME') now raises ValueError naming the existing member and
pointing at register_alias(), where it used to mint nothing and raise
nothing.

Census re-derived independently by AST over the merge commit rather than
taken from the PR table: 121 const modules hold 127 enum classes (three
modules define more than one class each). 6 classes already used
EnumRegistry from #855; of the other 115 modules, 105 share the generated
template byte-for-byte and are converted here, and the remaining 10 keep
their own bespoke __new__ and are left alone -- ftp/command (4 classes),
ftp/return_code (3), http/method, http/status_code, pcapng/option_type,
reg/apptype/apptype.py (2) and its four transport subclasses. Total now
inheriting EnumRegistry: 111 of 127 classes, up from 6. Also renames
pcapkit/corekit/enums.py to enum.py (no -s), the maintainer's ruling.

util/changelog_md.py regenerated CHANGELOG.md for all three entries added
across this and the two preceding commits (#855, #856, #858); --check
exit 0.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…- it was wrong

14c8930's commit message states: "the base's get() dispatch divergence
on a non-int/non-str key raises KeyError on the still-unconverted
generated template (empirically confirmed against 05468a0, not the
TypeError the PR body itself claims for that path)." That claim is false.
The test behind it called TransType.get(3.5) -- TransType is a generated
registry #855 never touched, so it only ever tells you about #858's
population, not #855's.

Re-tested properly this round: detached worktrees at 895cde6 (856,
immediately before 855) and 05468a0 (855), editable finder evicted from
sys.meta_path, pcapkit.__file__ asserted per process, one throwaway
process per cell since these registries mint on a fresh value.

  Flags.get(3.5) at 895cde6 (855's own six, pre-conversion):
    TypeError: argument of type 'float' is not a container or iterable
  Flags.get(3.5) at 05468a0 (855's own six, post-conversion):
    ValueError: 3.5 is not a valid Flags
  TransType.get(3.5) at both refs (generated, #855 never touches it):
    KeyError: 3.5 -- unchanged by #855, this is #858's population

So #855's PR body was right: TypeError is what the six bespoke registries
raised before this PR. KeyError belongs to the still-unconverted 105 and
is correctly attributed to #858's entry, not #855's. The #855 entry is
corrected back to TypeError, scoped to its own six registries' own
before/after rather than compared against "the generated template ...
used by the other 105" -- that comparison belongs to #858, which already
states it correctly.

14c8930 is left as-is rather than amended: it has already been pushed,
and rewriting it would need a force-push, which this branch does not do.
This commit is the correction of record instead.

util/changelog_md.py regenerated CHANGELOG.md; --check exit 0.
JarryShaw added a commit that referenced this pull request Sep 28, 2026
…s tense

Two fixes, both cross-review findings on #657.

1. The #855 entry's TypeError claim was generalised from one registry
(Flags) to all six #855 converts. The split is on member type, not on
converted-vs-not: the pre-855 hand-copied get() ends `return cls[key]`,
which aenum treats as a containment test on a Flag subclass (TypeError)
but a name lookup on a plain Enum (KeyError). Re-derived per-registry on
895cde6 (immediately before #855), MRO printed per cell:

  ExtensionHeader  [IntEnum, int]   get(3.5) -> KeyError: 3.5
  BindingACKFlag   [IntFlag, int]   get(3.5) -> TypeError: argument of
                                     type 'float' is not a container or
                                     iterable
  Flags            [IntFlag, int]   get(3.5) -> TypeError: ... (as above)

Confirmed post-855 (05468a0) that ExtensionHeader.get(3.5) now raises
ValueError, the same KeyError->ValueError shift #858's entry already
attributes to the other 105 -- so for ExtensionHeader specifically there
is no new divergence at all, only the same shift arriving six registries
early. Scoped the TypeError claim to the five IntFlag registries and
named ExtensionHeader's own KeyError->ValueError shift separately.

2. The #584 entry said -1 "was later replaced" by a sentinel object --
completed past tense for something that has not landed. gh pr view 859
-> OPEN, mergedAt null; origin/main:pcapkit/corekit/enum.py:64 still
reads NO_DEFAULT = -1. Reworded to present-continuous with an explicit
"not yet landed", so the entry does not tell a reader 1.5.0 ships
something it does not yet ship.

util/changelog_md.py regenerated CHANGELOG.md for both fixes.
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
@JarryShaw JarryShaw moved this to Done in PyPCAPKit 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 feat Pull requests that add a new capability (feat: subject prefix) refactor Restructuring for its own sake — neither a fix nor a new capability (refactor: prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant