Skip to content

schema: give every Schema subclass its own ABCMeta cache, and delete the version guard at its cause - #471

Merged
JarryShaw merged 2 commits into
mainfrom
fix-439-schema-abc-cache
Sep 18, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix-439-schema-abc-cache

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Closes #439.

SchemaMeta.__new__ branched on sys.version_info < (3, 11) and returned
type.__new__(...), bypassing abc.ABCMeta.__new__ and so never calling
_abc_init. The consequence was that no Schema subclass ever got its own
_abc_impl
— they shared collections.abc.Mapping's, since Schema inherits
Mapping. isinstance/issubclass against any schema class then answered from
a cache keyed on whatever had been asked first.

The code comment admitted it never understood why:

for unknown reason, the following code will cause an error for duplicated
keyword arguments in class definition.

Why the guard existed

abc.ABCMeta.__new__ names its fourth parameter namespace, and that parameter
is positional-or-keyword — not positional-only — before Python 3.11:

3.10.21  ABCMeta.__new__(mcls, name, bases, namespace, **kwargs)
3.11.15  ABCMeta.__new__(mcls, name, bases, namespace, /, **kwargs)

Seven Option subclasses in pcapkit/protocols/schema/misc/pcapng.py were
declared with a class keyword spelled namespace=, which bound that parameter
twice on 3.10 and earlier:

TypeError: ABCMeta.__new__() got multiple values for argument 'namespace'

That name collision — not anything about ABCMeta — is what the version guard
dodged, and the guard's < (3, 11) boundary is exactly where CPython added the
/.

The fix

SchemaMeta.__new__ is now an unconditional super().__new__(cls, name, bases, attrs, **kwargs), with the guard and the NOTE both deleted, and its own
name/bases/attrs made positional-only — the same fix CPython applied to
ABCMeta.__new__ in 3.11, which removes the collision structurally rather than
guarding it.

API change. Option subclasses take ns= instead of namespace=:
class Foo(Option, ns='opt') rather than class Foo(Option, namespace='opt').
The stored attribute Option.__namespace__ is unchanged, and code= — the other
class keyword this hierarchy uses — is unaffected. A repository-wide,
multi-line-aware search found nothing outside this repo's own seven subclasses
plus one test fixture using the old spelling; both are updated here.

And a guard so it cannot silently return

Because the collision class is not specific to namespace, SchemaMeta.__new__
now raises SchemaError if a class keyword collides, naming the offender.
_RESERVED_CLASS_KWARGS is {'mcls', 'name', 'bases', 'namespace', 'cls'}, and
the five are not all there for the same reason — measured with a metaclass whose
own parameters are positional-only, as SchemaMeta's now are:

                3.10.21                                          3.12.13
mcls/name/                                                       OK  (both sides
bases/namespace TypeError: ABCMeta.__new__() got multiple ...     positional-only)
cls             TypeError: __init_subclass__() got multiple ...   same TypeError

So mcls/name/bases/namespace collide only where ABCMeta's parameters are
not positional-only, while cls collides on every version through the
implicit __init_subclass__ classmethod binding — a different mechanism, and
independent of ABCMeta entirely. Covering all five is what makes this
long-standing rather than merely correct today. SchemaError had no caller before
this; it now has its first.

The guard earned its place immediately

A single-line grep sweep for the rename missed
tests/protocols/misc/test_pcapng_unit.py, where the declaration is split across
three lines ([^)]* cannot span newlines). It surfaced as the one failure in an
otherwise-clean 3.10 suite run — as SchemaError, exactly as designed — rather
than as a silently lost namespace. Fixed, and re-swept with a re.DOTALL pattern
across pcapkit/, tests/ and examples/ afterwards.

Required test cleanup

tests/protocols/test_option_roundtrip_unit.py instructed this fix to clean up
after itself — :43 "fixing #439 turns 3.10 red", :539 "which is the point",
and the guard test's own message "If #439 is fixed, delete that table and this
test." So INTERPRETER_GAPS, SCHEMA_ABC_IS_PER_CLASS,
test_schema_isinstance_is_interpreter_dependent, its _abc_impl helper, the
interpreter-dependence docstring section and the table's consistency-check
references are all gone. The seven PCAP-NG name-resolution cases it held are now
held to 'OK' by the main table's absence of an entry. The mh-extension,
httpv2-frame and hip-parameter entries were not touched.

One existing test in tests/protocols/schema/test_schema_unit.py mocked
sys.version_info to exercise the deleted branch; it is narrowed and renamed,
with a docstring saying why.

The real-world symptom

NameResolutionBlock.post_process asks isinstance(record, (IPv4Record, IPv6Record)) at pcapkit/protocols/schema/misc/pcapng.py:1248 and then reads
record.names. On the broken code that returned True for the block's
terminating EndRecord, so it read names off a record that has none — and every
NRB carries a terminating EndRecord. Reproduced with the two isinstance
questions real code actually asks, in order, with no dict/Mapping involvement
needed: True before, False after. Pinned as a regression test.

Verification

  • New tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py, 9 tests,
    checked against the pre-fix source so each fails there in the way its docstring
    predicts. The cache tests pin both directions of the corruption: asking
    Mapping first made isinstance({}, Schema) return True; asking Schema
    first made isinstance({}, Mapping) return False.
  • git merge-tree clean against both contested branches — fix-445-nested-packet-context
    and fix-463-hip-list-length-underflow — with no CONFLICT lines.
  • Full suite green: 3.10.21 at 922 passed / 0 failed / 109 skipped / 1465
    subtests; 3.14.7 at 939 passed with one failure traced to a missing
    typing_extensions in the ad-hoc venv, confirmed by installing it and rerunning
    that test alone.
  • mypy on 3.10.21: 108 errors before, 105 after — net −3, zero new. The removals
    are the return-value mismatch on the old type.__new__ branch and the
    unreachable/unused-ignore pair the version guard produced.

Findings not folded in

mypy 2.3.1 raises an internal error on this codebase when invoked under Python
3.14, reproducible on pre-fix code too — a pre-existing tooling gap, unrelated,
and there is no mypy job in .github/workflows/ that it affects.


Correction to this description

The API-change paragraph says the search "found nothing outside this repo's own
seven subclasses plus one test fixture" — that undercounts by one. The review
ran its own paren-depth-tracking multi-line sweep and found nine sites
carrying the old spelling: the seven Option subclass declarations, the
docstring example in pcapkit/protocols/schema/misc/pcapng.py, and the
three-line class declaration in tests/protocols/misc/test_pcapng_unit.py. The
docstring example was omitted from the count above.

The code is unaffected — all nine were updated, the post-fix sweep finds zero
remaining, and the ~35 unrelated EnumField/OptionEnumField/BitField
namespace= constructor parameters were left untouched. Only the prose count was
wrong.

…stance corruption (#439)

SchemaMeta.__new__ used to branch on sys.version_info < (3, 11) and call
type.__new__ directly on old Pythons, bypassing abc.ABCMeta.__new__ (and its
call to abc._abc_init) to dodge a class-keyword collision: several Option
subclasses in pcapkit/protocols/schema/misc/pcapng.py declared a namespace=
class keyword, which bound abc.ABCMeta.__new__'s own namespace parameter
twice on Python < 3.11 (that parameter became positional-only only in 3.11).

Bypassing ABCMeta.__new__ meant no Schema subclass ever got its own
_abc_impl; every one of them fell through the MRO to collections.abc.
Mapping's, so any isinstance(x, Mapping) or isinstance(x, Schema) question
about a plain dict poisoned the other's answer for the rest of the process,
in both directions. NameResolutionBlock.post_process (pcapng.py:1248) is the
real-world casualty: a terminating EndRecord tested True as an IPv4Record/
IPv6Record and AttributeError'd on record.names.

- Rename the colliding keyword: Option subclasses now spell it ns= instead
  of namespace= (pcapkit/protocols/schema/misc/pcapng.py). The stored
  attribute __namespace__ is unchanged. This is a public API change to
  Option's subclassing convention; nothing outside pcapkit/ was found using
  the old spelling.
- Delete the version guard: SchemaMeta.__new__ now calls
  super().__new__(...) unconditionally on every supported version.
- Add a guard that raises SchemaError, naming the offending keyword, when a
  Schema subclass is declared with a class keyword that collides with
  abc.ABCMeta.__new__ (mcls/name/bases/namespace) or with the implicit
  __init_subclass__ classmethod binding (cls) -- so the next accidental
  collision fails loudly here instead of several frames away. cls/name/bases
  are also now positional-only in SchemaMeta.__new__'s own signature,
  matching the fix CPython gave ABCMeta.__new__ in 3.11.
- tests/protocols/test_option_roundtrip_unit.py: delete INTERPRETER_GAPS,
  SCHEMA_ABC_IS_PER_CLASS and test_schema_isinstance_is_interpreter_dependent,
  which existed to record and pin this exact defect on Python 3.10; fixing
  #439 is what makes them obsolete.
- New regression tests in
  tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py, covering
  both corruption directions, the exact-type cache keying that makes probing
  order matter, the real pcapng.py:1248 symptom, and the new reserved-keyword
  guard.

Build: brazil-build equivalent (pip install -e '.[test,Scapy]') green on
3.10.21 and 3.14.7; mypy clean of new errors on 3.10.21.
@JarryShaw

Copy link
Copy Markdown
Owner Author

What I read

gh pr view 471 --comments (no inline review comments exist yet — pulls/471/comments and pulls/471/reviews both return []), gh issue view 439 --comments including comment 5725158653 (the "duplicated keyword arguments NOTE is explained" root-cause comment), the full diff git diff origin/main...6b5505fb2 (single commit, 6 files, matches the PR body exactly), and git show fa128959e:pcapkit/protocols/schema/schema.py / .../misc/pcapng.py as the pre-fix baseline. gh pr checks 471 is now all green: Python 3.10 pass (11m56s), Integration Python 3.10 pass (17m3s), and every other Python/Compat/Integration job pass; Docs test gate and Gate (full suite, Python 3.14) skip by design on pull_request; the blank-rollup pyup.io/safety-ci entry is a pass with no vulnerable deps.

1. The reserved-keyword set

Verified the two collision mechanisms independently, not by re-deriving the author's table.

Mechanism A — ABCMeta.__new__'s own params, pre-3.11 only. Built a synthetic metaclass (no pcapkit) with positional-only name/bases/attrs, matching SchemaMeta.__new__'s new signature, and tried each candidate name as a class keyword on 3.10.21/3.11.15/3.12.13/3.14.7:

                 3.10.21                                              3.11.15+
mcls/name/bases/ TypeError: ABCMeta.__new__() got multiple values     OK
namespace        for argument '<name>'
cls, code, ns,   OK                                                   OK
attrs

This exactly reproduces the PR's claimed table (mcls/name/bases/namespace collide only pre-3.11; attrs never collides because ABCMeta.__new__'s 4th positional param is literally named namespace, not attrs).

Mechanism B — cls via the implicit __init_subclass__ classmethod binding, on every version. My first synthetic attempt (declaring cls positional-only in a toy __init_subclass__) wrongly showed "OK" everywhere — so I went straight to the real class the PR names, typing.Generic:

3.10.21: TypeError: Generic.__init_subclass__() got multiple values for argument 'cls'
3.11.15: TypeError: Generic.__init_subclass__() got multiple values for argument 'cls'
3.12.13: TypeError: _generic_init_subclass() got multiple values for argument 'cls'
3.14.7:  TypeError: _generic_init_subclass() got multiple values for argument 'cls'

Confirmed on every supported interpreter, independent of the ABCMeta fix — the PR's claim that cls "collides on every version through a different mechanism" holds.

Completeness / over-breadth. Grepped the whole tree for mcls=/bases= as class keywords (none exist, so no false rejection risk today), and swept for any other name that might collide — nothing else did (kwargs, args, dict, kwds, self, o all either work fine or fail for the pre-existing, unrelated reason that object.__init_subclass__ rejects any unconsumed kwarg, which is not the opaque-collision failure mode this guard targets). I found no missing sixth name and no member of the five that is harmless/over-broad in this codebase. _RESERVED_CLASS_KWARGS = frozenset({'mcls', 'name', 'bases', 'namespace', 'cls'}) at pcapkit/protocols/schema/schema.py:249 is correct and not over-broad.

SchemaError choice. pcapkit/utilities/exceptions.py:377: class SchemaError(BaseError, ValueError), docstring "Invalid schema." — an existing, previously-uncalled exception (confirmed via grep -rn SchemaError pcapkit/ tests/: only its own definition and __all__ entry predate this PR). Reusing it for "this class declaration is invalid" fits its stated purpose exactly, and it sits in the same ValueError family as sibling FieldValueError/SeekError. The raise at schema.py:271-279 names the offending class ({name}) and the sorted offending keyword(s), and explains both collision mechanisms plus points at #439 — a good, actionable message.

2. Both directions of the cache corruption

Extracted tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py (352 lines, 9 tests) from 6b5505fb2 and ran it for real, in both directions, using a from-scratch Python 3.10.21 venv (mise interpreter + pip install tbtrim aenum chardet dictdumper pytest) with PYTHONPATH forced to the worktree root (verified pcapkit.__file__ resolves under the worktree before every run).

Against the fixed PR code (checked out pcapng.py+schema.py from 6b5505fb2 into the worktree, then reverted with git checkout HEAD -- afterward — working tree confirmed clean throughout, see below):

9 passed, 1 warning, 5 subtests passed in 5.05s

Against pre-fix schema.py/pcapng.py (git checkout HEAD -- ... restores origin/main's versions) with the new test file kept:

7 failed, 2 passed

The 2 that still pass on broken code are test_probing_the_same_exact_type_reproduces_439s_shape and test_probing_a_different_exact_type_gives_a_false_negative — by design: both build their own hand-rolled abc.ABCMeta-derived hierarchy independent of SchemaMeta, specifically to demonstrate the general mechanism (and, in the second case, the "different exact type = cache miss = false negative" pitfall) rather than to pin the actual regression. That's a deliberate, well-labeled non-regression-test, not a gap.

The 7 that fail all fail for exactly the reason their docstrings claim:

  • test_a_plain_dict_is_not_a_schema_after_mapping_is_asked_first: AssertionError: True is not false at line 87 (isinstance({}, Schema) wrongly True after Mapping asked first) — queries exact dict via {}.
  • test_asking_schema_first_does_not_break_mapping_for_a_plain_dict: AssertionError: False is not true at line 111 (isinstance({}, Mapping) wrongly False after Schema asked first) — the reverse direction, also exact dict.
  • test_a_real_instance_still_isinstance_of_its_own_class: AssertionError: {} is an instance of <class '...EndRecord'> at line 150 — exact dict.
  • test_schema_subclasses_have_their_own_abc_impl: AssertionError: unexpectedly identical: <_abc._abc_data object ...> at line 127 (EndRecord._abc_impl is Schema._abc_impl) — direct mechanism check, not isinstance-order-dependent.
  • test_the_pcapng_1248_symptom_no_longer_reproduces: AssertionError: True is not false : a terminating EndRecord must not test as an IPv4Record/IPv6Record... at line 279 — this one deliberately needs no dict/Mapping at all, matching the real call site.
  • test_an_ordinary_class_keyword_is_unaffected: AssertionError: None != 'opt' — ns= silently does nothing pre-fix (expected; ns= didn't exist before).
  • test_each_reserved_keyword_raises_a_named_schema_error: AttributeError: type object 'SchemaMeta' has no attribute '_RESERVED_CLASS_KWARGS' — the guard itself is new.

Both directions are genuinely pinned, and every reproducer that claims to need exact-type dict actually uses {}/dict() rather than a subclass or ChainMap.

3. Test cleanup in test_option_roundtrip_unit.py

Diff confirms INTERPRETER_GAPS, SCHEMA_ABC_IS_PER_CLASS, test_schema_isinstance_is_interpreter_dependent, the _abc_impl helper, the "When the outcome depends on the interpreter" docstring section, and the INTERPRETER_GAPS consistency-check subtests are all gone; _gap_for no longer branches on SCHEMA_ABC_IS_PER_CLASS. The three ns_dns* entries stay in EXPECTED_FAILURES untouched (their self._opt counter cause is unrelated to #439 and interpreter-independent) — confirmed via git diff showing no changes to those lines.

Rather than trust "suite green," I enumerated module.cases() directly (loading examples/generators/options.py the same way the test's _load_generator does) and ran roundtrip() on the 7 specific labels the deleted table held, on Python 3.10.21 with the fixed code:

total cases: 322
pcapng-block/Name_Resolution_Block  status='OK'         detail=''
pcapng-option/ns_dnsIP4addr_3       status='CONSTRUCT'  detail='...option must be only one, but 2 found.'
pcapng-option/ns_dnsIP6addr_4       status='CONSTRUCT'  detail='...option must be only one, but 2 found.'
pcapng-option/ns_dnsname_2          status='CONSTRUCT'  detail='...option must be only one, but 2 found.'
pcapng-record/nrb_record_end        status='OK'         detail=''
pcapng-record/nrb_record_ipv4       status='OK'         detail=''
pcapng-record/nrb_record_ipv6       status='OK'         detail=''

The 4 cases with no override now genuinely pass 'OK' on 3.10 (not just "no longer excused" — actually green), and the 3 ns_dns* cases fail for exactly the pre-existing, interpreter-independent reason the main EXPECTED_FAILURES table already records ('option must be only one', matching pcapkit/protocols/misc/pcapng.py:1078/:1088). Full suite confirmation: tests/protocols/test_option_roundtrip_unit.py on 3.10.21 fixed code — 6 passed, 1 warning, 358 subtests passed in 0.83s.

4. Rename blast radius

Wrote an independent multi-line-aware (paren-depth-tracking, not single-line-regex) sweep for class \w+\(...namespace=...\) at class-header depth 1, distinct from EnumField(namespace=...)/OptionEnumField(namespace=...)/BitField(namespace=...)/self._namespace.get(..., namespace=...) calls (373 raw namespace= hits in the tree, ~35+ of them these unrelated field-constructor/method-call parameters that must stay untouched — spot-checked several, e.g. pcapng.py:324,484,546,635,736,782,877,904 — all EnumField/OptionEnumField/BitField/method-call sites, correctly unrenamed).

Against origin/main my sweep found 9 class-keyword sites, one more than the PR body's "seven subclasses plus one test fixture" prose:

pcapkit/protocols/schema/misc/pcapng.py:424   class NewOption(Option, namespace='opt', code=...)   <- docstring example
pcapkit/protocols/schema/misc/pcapng.py:475,626,895,1162,1269,1573,1646   <- the 7 real _XXX_Option classes
tests/protocols/misc/test_pcapng_unit.py:1262  class LocalIterableOption(schema_pcapng.Option,
                                                    code=[...],
                                                    namespace='localtest')   <- the 3-line one the guard caught

The 9th (the docstring example at pcapng.py:424) is actually renamed in the diff (class NewOption(Option, ns='opt', code=...)) — it's just not separately counted in the PR body's prose ("seven subclasses plus one test fixture"). Purely a description undercount, not a missed site: running the same sweep against 6b5505fb2 finds 0 remaining class-keyword namespace= sites anywhere under pcapkit/, tests/, examples/.

Option.__namespace__ (the stored attribute) is untouched — confirmed by test_an_ordinary_class_keyword_is_unaffected passing and by grep showing cls.__namespace__ = ns (pcapng.py:432) is the only place it's set.

5. Real-world symptom

Covered under point 2 — test_the_pcapng_1248_symptom_no_longer_reproduces fails with True is not false pre-fix and passes post-fix, on real Python 3.10.21, reproducing NameResolutionBlock.post_process's isinstance(record, (IPv4Record, IPv6Record)) at pcapkit/protocols/schema/misc/pcapng.py:1248 exactly as described, with no dict/Mapping needed. Confirmed clear.

6. The narrowed test_schema_unit.py test

git diff shows test_schema_final_generated_init_and_legacy_version_branch renamed to test_schema_final_generated_init, dropping the mock.patch('pcapkit.protocols.schema.schema.sys.version_info', (3, 10)) block and its assertion. Checked whether this is even still possible to run: schema.py no longer imports sys at all (git diff shows -import sys with no replacement), so the old test would now fail with AttributeError (module has no attribute sys) if left in place — narrowing wasn't optional, it was forced by deleting the thing being mocked. Nothing of enduring value is lost: the removed assertion existed solely to prove the now-deleted type.__new__ branch still produced correct __fields__, and that branch doesn't exist to test anymore. Docstring explains this at the test correctly.

Measurements against the author's claimed numbers

All measured myself, in a from-scratch Python 3.10.21 venv and separately in the repo's existing Python 3.14.7 .venv (both already had typing_extensions installed, so I did not hit the ad-hoc-venv gap the author reported), with fixtures generated via examples/generators/make_samples.py under a forced PYTHONPATH, pcapkit.__file__ verified to resolve inside the worktree before every run:

  • 3.10.21, full suite, fixed code: 940 passed, 91 skipped, 263 warnings, 1473 subtests passed in 708.54s. 0 failures, matching the headline claim; the author's own 922/109/1465 numbers differ (more skips, fewer passes) — expected, since their container reportedly lacked more optional deps (RUNTIME_DEPS-gated tests and others vary by environment, as flagged in the review brief; not a discrepancy I can resolve or should expect to match).
  • 3.14.7 (repo .venv), full suite, fixed code: 1014 passed, 17 skipped, 11115 warnings, 1553 subtests passed in 821.67s. 0 failures — no typing_extensions-related failure surfaced, consistent with the author's own explanation that theirs was an ad-hoc-venv artifact rather than a real defect.
  • mypy 2.3.1 on 3.10.21, pre-fix (git checkout HEAD -- schema.py pcapng.py, i.e. origin/main): Found 108 errors in 33 files (checked 496 source files).
  • mypy 2.3.1 on 3.10.21, fixed (git checkout 6b5505fb2 -- schema.py pcapng.py): Found 105 errors in 33 files (checked 496 source files).
  • Diffing the two error lists line-by-line: the only substantive change is the disappearance of schema.py:241:20 Incompatible return value type ... [return-value], schema.py:242:9 Statement is unreachable [unreachable], and schema.py:242 Unused "type: ignore" comment [unused-ignore] — exactly the return-value mismatch and unreachable/unused-ignore pair the PR body names, net −3, zero new errors (the two remaining schema.py lines are the same pre-existing __prepare__-override and missing-self-argument complaints, just shifted by line number). Author's 108→105/net−3/zero-new claim fully confirmed.
  • git merge-tree (git 2.47.3, two-ref mode) against both fetched contested branches: fix-445-nested-packet-context → clean tree bf1bfea9c0ad9b9b725aab007e78171ddf190e19, no CONFLICT; fix-463-hip-list-length-underflow → clean tree 8c12c2fd53ca7eec6aec5562e2fb53dd44079b2f, no CONFLICT. Confirmed.
  • CI: re-ran gh pr checks 471 after the earlier "pending" snapshot — now all pass, including the two that matter most here, Python 3.10 (11m56s) and Integration Python 3.10 (17m3s), i.e. the real CI actually exercised the exact defect scenario on the exact interpreter and came back green.

I could not check anything beyond bare-3.10 metaclass mechanics on genuinely dependency-free 3.10 (I built a full dependency venv instead, which worked, so this caveat ended up moot) — no other gaps to disclose.

Worktree hygiene

All temporary reverts (git checkout HEAD -- <files> / git checkout 6b5505fb2 -- <files>) were undone with git reset --hard HEAD; git status --short is empty and git diff origin/main is empty, confirming the worktree ended exactly where it started (fa128959e, base of #471).

API change verdict

class Foo(Option, namespace='x') → class Foo(Option, ns='x') is a real, breaking rename of a public extension-point keyword (Option.__init_subclass__ is documented with a worked example precisely because third parties are expected to subclass it). It is justified: the old name is not cosmetic, it is the literal cause of #439, and keeping it while working around the collision (the rejected abc._abc_init alternative) would have meant carrying a private-CPython-API workaround indefinitely for a name nobody has to keep. It is correctly scoped: my independent multi-line-aware sweep found the same 9 sites the author's did (7 declarations + 1 docstring example + 1 test fixture) and confirmed 0 remaining anywhere in pcapkit/, tests/, examples/ post-fix, while the ~35+ unrelated namespace= field-constructor parameters are untouched. There's no deprecation shim (dual-accepting namespace= with a warning for one release) — for a break this narrowly scoped and this clearly tied to a real, silent-corruption defect, I think a hard break with a named, actionable SchemaError is the right tradeoff over a transition period that would have to keep tolerating the collision.

Verdict

GOOD TO MERGE — sha 6b5505fb2.

@JarryShaw
JarryShaw merged commit 5182ad0 into main Sep 18, 2026
23 checks passed
JarryShaw added a commit that referenced this pull request Sep 18, 2026
The owner asked for the dict subclass gone in favour of ChainMap,
which is where this design started before #439 was (wrongly)
suspected of being disturbed by it. Two premises settled first:

- The fallback container is still required: #471 (fixing #439
  directly) does not touch pcapkit/corekit/fields/misc.py at all, and
  #445's own KeyError symptom persists on #471's tree without this
  fix. Different defects.
- ChainMap never poisoned anything. The shared ABCMeta cache keys on
  the exact type queried: asking about a ChainMap instance caches
  ChainMap, not dict, and isinstance({}, Schema) stayed False
  afterwards. The actual poisoner was ordinary code asking isinstance
  about a plain dict (Info.__update__), unrelated to what this
  function returns either way. #471 has also now given every Schema
  subclass its own _abc_impl, removing the mechanism regardless.

nested_packet_context() now returns a bare collections.ChainMap({'__packet__':
packet}, packet) -- no custom class. This gets, with no code: setdefault
honouring the fallback (the dict subclass's own setdefault bypassed
__missing__ and would insert a name locally instead of returning the
parent's value -- caught directly, see the test below), Mapping's
__eq__ instead of dict's, and dict(**pkt)/.copy() working correctly out
of the box.

Only one thing needed a local fix rather than a class: ChainMap is not
nominally a dict, so `value.pack(nested_packet_context(packet))` no
longer satisfies Schema.pack's `dict[str, Any]` annotation. Widening
that annotation cascades into every other field class's pack/unpack,
which forward the same packet argument with their own dict[str, Any]
annotations (measured: +7 new mypy errors). Used an explicit
cast('dict[str, Any]', ...) at the one call site instead -- asserting
that ChainMap satisfies every operation the annotation promises
(subscript, in, .get(), iteration), which it does, rather than
widening the annotation or reintroducing a fresh type: ignore.

tests/corekit/test_fields_misc_packet_context.py's docstring already
named collections.ChainMap (stale by coincidence during the dict
detour, accurate again now) -- expanded it to say so deliberately, and
added explicit setdefault/.copy() assertions to
test_nested_schema_reads_enclosing_field_by_name_and_does_not_leak_writes,
confirmed to fail under the retired dict subclass (setdefault returned
999, shadowing the parent's 3, instead of honouring it).

Re-verified all ten EXPECTED_FAILURES entries deleted across the dict
detour -- six httpv2-frame, four mh-extension -- individually via
options.roundtrip(), not inferred from a green suite: all ten still
return 'OK' against the ChainMap version.

Merged origin/main (5182ad0, #471) first -- clean, no conflicts.

Verified: mypy pcapkit -> 124 errors/40 files (a fresh main at
5182ad0: 125, one more from an unrelated pre-existing gap this
branch already cleans up). Round-trip harness: 6 passed, 358 subtests,
0 failed. tests/protocols/misc/test_pcapng_unit.py +
tests/protocols/internet/test_mh_unit.py + tests/protocols/schema/:
97 passed, 416 subtests, 0 failed.
@JarryShaw
JarryShaw deleted the fix-439-schema-abc-cache branch September 18, 2026 20:22
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Schema subclasses share one ABCMeta cache on Python <=3.10, so isinstance/issubclass return whichever answer was asked first

1 participant