corekit: let a nested schema's field callbacks reach the enclosing schema - #457
Conversation
…hema
- SchemaField.pack/unpack handed a nested schema a context of only
{'__packet__': packet}, so a callback written the ordinary way --
length=lambda pkt: pkt['length'] -- raised KeyError as soon as a schema
was nested. Measured casualty: CGA Parameters (mh.py
CGAParameter.extensions), unparsable via the public API.
- Fix: nested_packet_context() replaces that literal with a two-level
collections.ChainMap, so a name absent locally falls through to the
enclosing schema, while __packet__ still reaches it explicitly.
ChainMap.__setitem__/__delitem__ always act on the nested map, so a
write never reaches the parent and a shadowed name is read locally.
- mh.py needed no change: CGAParameter.extensions's pkt['length'] now
resolves via the fallback. CGA Parameters still does not parse -- it
now reaches FieldValueError: Field parameters has invalid length
(#446, ForwardMatchField vs. Schema.__len__), which is not fixed here.
- pcapng.py's two __packet__ consumers are untouched: real callers hand
them a plain {'__packet__': {...}} dict rather than going through
SchemaField, so their hand-rolled fallback still has to handle that
shape and is not redundant with this change.
- Documented the __packet__ contract in nested_packet_context, and added
it to Schema.unpack's reserved-key list alongside __length__ and
__option_padding__.
- The same latent defect affected six HTTP/2 frame schemas (reading the
header's 'flags' the same way); updated
tests/protocols/test_option_roundtrip_unit.py's EXPECTED_FAILURES:
removed PUSH_PROMISE and PING (now round-trip cleanly), and
re-attributed Multi_Prefix, DATA, HEADERS, CONTINUATION and SETTINGS to
the distinct defects this fix newly exposes underneath.
Ran PYTHONSAFEPATH=1 python -m pytest tests -q at baseline e2d8ed6
(960 passed, 17 skipped, 1264 subtests) and after (see PR description).
33c7413 to
46fe59a
Compare
|
Reviewing on behalf of Copilot (out of tokens). Head CISettled at 19 SUCCESS / 2 FAILURE / 2 SKIPPED:
The design: chained lookup via
|
- The previous NestedPacketContext used collections.ChainMap directly. ChainMap is itself a collections.abc.MutableMapping, and constructing one was enough to disturb the shared _abc_impl cache that every Schema subclass uses on CPython <= 3.10 (#439): a later, unrelated isinstance(some_dict, Schema) check in test_pcapng_remaining_constructor_branches_and_custom_dispatch flipped from False to True, raising AttributeError: 'dict' object has no attribute 'to_dict' from Schema.to_dict (schema.py:520). - Measured directly: with everything else unchanged, reverting only the ChainMap call back to a plain {'__packet__': packet} literal makes that test pass again on Python 3.10; restoring it reproduces the failure. Confirmed both in an isolated single-test run and via a local Python 3.10 venv (943 passed, 0 failed after this commit, against a failure before it). - Fix: NestedPacketContext is now a plain class implementing __getitem__/__setitem__/__delitem__/__contains__/__iter__/__len__/ get/update/copy/keys/values/items by hand, with none of it deriving from collections.abc. Same two-level fallback, same __packet__ reachability, same write isolation as before -- only the mechanism changes, not the contract. All existing tests pass unchanged. - Not a fix to #439 itself, which remains filed and untouched; this only stops this PR's own code from being the thing that trips it. Verified: PYTHONSAFEPATH=1 pytest tests -q on Python 3.14 (981 passed, 17 skipped, 1267 subtests, 0 failed) and on a local Python 3.10 venv (943 passed, 55 skipped, 1237 subtests, 0 failed -- the skip count differs only because that venv lacks some optional runtime deps).
…ation - NestedPacketContext was a hand-written class implementing the mapping protocol from scratch, deriving from nothing -- correct, but it left Schema.pack/unpack's "packet: dict[str, Any]" annotation unsatisfied, which is what every other field class's pack/unpack still declares. Widening those annotations to admit the new type cascades through every field class that forwards packet along (ListField, ConditionalField, FieldBase.__call__, pre_process/post_process, ...), well outside this fix's file list, for seven new mypy errors net. - Fix: NestedPacketContext now subclasses dict directly. dict's own metaclass is plain "type", not ABCMeta, so subclassing or instantiating it never touches the shared _abc_impl cache that #439 is about -- confirmed again on the Python 3.10 venv (this test passes both alone and in the full pcapng module: 37 passed, 139 subtests). Being a real dict also nominally satisfies every existing "dict[str, Any]" annotation, so no other file needs to change. - __missing__ gives the enclosing-schema fallback for free, since dict.__getitem__ calls it automatically when a key is absent locally. - __contains__ and get are overridden because the dict built-ins for both bypass __missing__ entirely. - __iter__/__len__/keys/values/items are overridden for the union-of-both- levels semantics the design promises; dict's own versions would only see this instance's local keys. - copy is overridden because dict.copy() always returns a plain dict, even for a subclass, which would silently drop the fallback. - Plain assignment/deletion need no override: dict's own __setitem__/ __delitem__ already only touch this instance's own storage. - Removed the now-genuinely-unused "type: ignore[has-type]" this rewrite exposed on SchemaField.length -- confirmed present and already-unused on a clean origin/main (da24227) checkout too, so this is a pre-existing, unrelated mypy hygiene gap this rewrite happened to touch, not something introduced by it. Net mypy count: 123 (down from main's own 124), with no new errors from this branch's own code. - Fixed a stale citation: the EOFError-on-zero-length Gap entries pointed at decorators.py:222; the actual "raise EOFError" is at :228 (prepare itself starts at :177, and #450 shifted lines since the citation was written). Verified: mypy pcapkit -> 123 errors/40 files (main: 124; the diff is the one pre-existing unused-ignore above, not a new error). pytest tests -q unaffected on Python 3.14. Local Python 3.10 venv: test_pcapng_remaining_constructor_branches_and_custom_dispatch passes alone and as part of the full misc/test_pcapng_unit.py module.
…-context # Conflicts: # tests/protocols/test_option_roundtrip_unit.py
- main merged #437 (MH registry completion, including the four mh-extension codes and the _make_ext_multiprefix arithmetic fix) and #456/#446 (the ForwardMatchField double-count in Schema.__len__) since this branch's last merge. Combined with this PR's own fix, all four mh-extension/{Multi_Prefix,Exp_FFFD,Exp_FFFE,Exp_FFFF} cases now round-trip cleanly -- verified directly against the round-trip harness (all four return 'OK'), not assumed from the PR descriptions. Deleted their EXPECTED_FAILURES entries; a stale PARSE/KeyError expectation would otherwise have failed this module outright, per its own two-way assertion. - The issue's own 40-octet CGA Parameters reproduction now parses completely end to end (confirmed directly: MH(raw, len(raw), extension=True) returns a populated CGAParametersOption, no exception). Rewrote test_cga_parameters_option_reaches_the_446_boundary _not_a_keyerror, which asserted the (now stale) FieldValueError boundary, as test_cga_parameters_option_now_parses_end_to_end, asserting the parsed fields directly. - #437 had pinned the pre-fix KeyError as test_mh_cga_parameters_option_is_unparsable_upstream, explicitly so that "whoever fixes it finds out here" -- and it did: this run turned that test red once the merge above landed. Replaced it with test_mh_cga_parameters_option_now_parses, asserting the option parses and its fields are what the wire says, and fixed the now-stale cross-reference and claim in test_mh_pmipv6_options_round_trip_byte_for_byte's docstring (CGA_Parameters is still excluded from that test's cases, but no longer because it cannot be parsed -- that is now a separate, deliberate scope decision for whoever adds its full round-trip identity). - Merged origin/main (0283a6d) with one conflict, in this exact region of tests/protocols/test_option_roundtrip_unit.py, resolved by re-deriving the correct entries from the actual post-merge behaviour rather than picking either side. Verified: mypy pcapkit -> 123 errors/40 files (a fresh main, 0283a6d, is 124 -- unchanged from before this merge). Round-trip harness: 7 passed, 363 subtests passed, 0 failed (up from 299 subtests before #437 grew the mh-extension family to four codes). tests/protocols/ internet/test_mh_unit.py: 35 passed, 266 subtests passed, 0 failed. Full local suite result to follow in the PR description.
| return self._field.unpack(buffer, packet) | ||
|
|
||
|
|
||
| class NestedPacketContext(dict): |
There was a problem hiding this comment.
what about use an Info subclass? and im not sure why must we use a dedicated class for this.
There was a problem hiding this comment.
Measured rather than argued from the abc angle (that angle turned out to be a
dead end -- see below), by actually building an Info-based nested context
and running it: it does not break Python 3.10. Swapped NestedPacketContext
for an Info subclass on disk, ran it on a local Python 3.10.21 venv (matching
CI's exact patch), and tests/protocols/misc/test_pcapng_unit.py -- the module
that broke under the ChainMap version -- came back clean: 37 passed, 139
subtests passed, 0 failed, both for the one previously-failing test alone and
for the whole module.
So the earlier "avoids a Mapping tie" reasoning I gave for the dict choice
doesn't hold up, and I'm not using it any more: Info is itself Mapping-based
and gets its own _abc_impl regardless (its metaclass has no CPython-version
bypass), so a Mapping tie alone was never the risk. I'm not citing a
mechanism I haven't personally re-verified, so I'll leave the actual cache
mechanics to whoever measured that -- what I can say directly is that Info
passes the test that mattered.
That still leaves the real question: not "is it safe" but "does it fit". I
built the Info version to answer this honestly rather than guess, and here
is what it would and wouldn't give for free:
__missing__fallback: no saving.Info.__getitem__is
self.__dict__[self.__map__.get(name, name)]-- a from-scratch
implementation with no__missing__-style hook (that's adictC-level
feature, not a generalMappingone). Whether I subclassdictorInfo,
I have to write the fallback lookup myself; there's no version whereInfo
saves me this code.__contains__/get: a genuine point inInfo's favour.Mapping
supplies mixin implementations of both that delegate to__getitem__, so
once__getitem__has the fallback,inand.get()inherit it for free.
dict's own__contains__/getare C-level and bypass__missing__
entirely, so myNestedPacketContext(dict)has to override both by hand.
Infowould save that code.- Writes must land on the nested instance only, never on the enclosing
schema -- this is where it breaks down.Infois deliberately immutable:
__setattr__raisesUnsupportedCall, and it has no__setitem__at all
(it inherits read-onlyMapping, notMutableMapping). Field callbacks
write into the packet dict constantly during parsing --
Schema.unpack's own per-field loop doespacket[field.name] = valueonce
per field, for every field of every nested schema. MyInfoscratch class
only supports this by writing intoself.__dict__directly from a custom
__setitem__, bypassing the immutabilityInfo's own docstring promises
("Info objects are immutable, thus cannot set or delete attributes after
initialisation"). It works, but it's a subclass that quietly defeats the
guarantee the class exists to provide -- not an extension ofInfo's
contract, a contradiction of it. - Extra bookkeeping to filter out:
Infoinstances carry__map__and
__map_reverse__inself.__dict__for its builtin-name-collision
handling, which then show up in naive iteration alongside the real packet
keys (I hit this directly --set(packet.keys())came back with_parent,
__map__and__map_reverse__mixed in with the actual field names, and
I'd have needed to filter them the wayInfo.__iter__filters
self.__excluded__). A plaindictsubclass has no such baggage: its own
storage holds only what's explicitly put there. - Purpose mismatch, not just mechanics: every real
Infosubclass in this
codebase declares a fixed, type-annotated field set and is built once as a
stable snapshot (that's whatinfo_final/__new__are for). A nested
packet context is the opposite: one shared, generic type instantiated fresh
per parse, holding whatever field names that schema happens to declare,
mutated field-by-field as parsing proceeds. Reusing the bareInfoclass
works because it happens to accept arbitrary kwargs, not because that's
what it's for.
So: two of the four things this needs (__contains__/get) Info gives for
free; one (__missing__) is a wash; and the write-isolation requirement is a
real contract conflict, not just extra code, because Info advertises
immutability as a feature and a nested context needs exactly the opposite.
That's why I kept the small dedicated class rather than switching -- "least
code that gets the contract right without fighting another class's
guarantees," not "because that's how I already did it." If the immutability
conflict is judged acceptable to paper over (my scratch class shows it's
mechanically possible), I'll switch; I don't think it should be, given the
class's own docstring says the opposite of what the subclass would then do.
There was a problem hiding this comment.
Yes, still required — and that is now measurable directly rather than by argument, because main already contains #471 (merged as 5182ad0ce).
Two separate facts:
#471 does not touch this file at all. git diff origin/main~1..origin/main --stat for #471's merge lists no change to pcapkit/corekit/fields/misc.py. It fixed #439 by giving every Schema subclass its own _abc_impl; it never addressed a nested schema's inability to reach the enclosing packet's fields by name.
#445's own symptom persists on a tree that has #471. Reverting only pcapkit/corekit/fields/misc.py to origin/main's version — leaving everything else on this branch, including all of #471 — fails 4 tests:
FAILED tests/corekit/test_fields_misc_packet_context.py::CGAParametersRegressionTests::test_cga_parameters_option_now_parses_end_to_end
FAILED tests/corekit/test_fields_misc_packet_context.py::NestedPacketContextTests::test_pcapng_block_type_mismatch_consumer_still_works_with_both_shapes
FAILED tests/corekit/test_fields_misc_packet_context.py::NestedPacketContextTests::test_pcapng_byteorder_consumer_still_works_with_both_shapes
4 failed, 5 warnings in 2.99s
Restore the file and the same selection is 101 passed, 416 subtests, 0 failed. Different defects.
Separately: the dict subclass is gone, as of 82dbf9416. nested_packet_context() now returns a bare collections.ChainMap({'__packet__': packet}, packet) — no dedicated class at all, which is where this started before ChainMap was (wrongly) suspected of disturbing #439.
That suspicion was wrong, and it is worth stating plainly since it is what drove the detour: the ABCMeta caches key on the exact type queried, not on ancestry. Asking about a ChainMap instance caches ChainMap, not dict. The actual poisoner was ordinary code asking isinstance about a plain dict — pcapkit/corekit/infoclass.py:270 does isinstance(dict_, (dict, collections.abc.Mapping)) — which a ChainMap-based context never did either way. And #471 has since removed the mechanism regardless.
What dropping the class buys, with no code to maintain:
| operation | retired dict subclass |
ChainMap |
|---|---|---|
setdefault('length', 999) where the parent has 3 |
999 — bypassed __missing__ and inserted locally |
3 |
ctx == dict(ctx) |
False — dict.__eq__ saw only __packet__ |
True |
pop('length') |
bare KeyError |
KeyError "Key not found in the first mapping: 'length'" — an explicit refusal |
copy() |
needed an override or it silently dropped the fallback | native, keeps the parent, writes stay local |
Each measured on a fresh context, because these mutate and a reused one contaminates the next reading. The first three rows are the substance of #474, which this therefore closes out.
Write isolation is unchanged and still holds: ctx['length'] = 99 leaves the enclosing dict at 3, ctx['newname'] = 5 never appears in it, and del ctx['length'] falls back to 3 rather than deleting the parent's.
One thing needed a local fix rather than a class: a ChainMap is not nominally a dict, so it does not satisfy Schema.pack's dict[str, Any] annotation. Widening that annotation cascades into every other field class's pack/unpack (measured: +7 new mypy errors), so there is a single explicit cast('dict[str, Any]', ...) at the one call site instead, asserting that ChainMap supports every operation the annotation promises.
The ten EXPECTED_FAILURES entries this branch deletes were re-verified individually rather than inferred from a green suite — cases() enumerates 322 cases, all ten labels are present in it (six httpv2-frame, four mh-extension), and each returns OK: 87 entries on main down to 77 here.
There was a problem hiding this comment.
A correction to my own earlier reply in this thread, since the review measured it more carefully than I did.
I wrote that reverting only pcapkit/corekit/fields/misc.py to origin/main's version fails 4 tests. It fails 5. My run had reverted the file but then executed only tests/corekit/test_fields_misc_packet_context.py, so it never reached the fifth. Re-run across the whole targeted selection:
FAILED tests/corekit/test_fields_misc_packet_context.py::NestedPacketContextSemanticsTests::test_nested_schema_reads_enclosing_field_by_name_and_does_not_leak_writes
FAILED tests/corekit/test_fields_misc_packet_context.py::NestedPacketContextSemanticsTests::test_pcapng_block_type_mismatch_consumer_still_works_with_both_shapes
FAILED tests/corekit/test_fields_misc_packet_context.py::NestedPacketContextSemanticsTests::test_pcapng_byteorder_consumer_still_works_with_both_shapes
FAILED tests/corekit/test_fields_misc_packet_context.py::CGAParametersRegressionTests::test_cga_parameters_option_now_parses_end_to_end
FAILED tests/protocols/internet/test_mh_unit.py::MHUnitTests::test_mh_cga_parameters_option_now_parses
5 failed, 96 passed, 23 warnings, 416 subtests passed
The fifth is test_mh_unit.py::MHUnitTests::test_mh_cga_parameters_option_now_parses — a second, independent witness to the #445 symptom, in a different file from the rest. The error was in my favour, which is the more insidious direction: it understated the evidence that this fix is load-bearing.
Restoring the file gives 101 passed, 416 subtests, 0 failed on the same selection. The corrected figure is in the PR body as well.
PR #462 merged (bind HTTP.make to a real instance, and wrap SETTINGS' item schema, closing #459) since this branch's last merge, and pulled in via the fast-forward to 0e7abbe. SettingsFrame.settings now wraps its item_type in SchemaField(schema=SettingPair) instead of passing the raw class, so the AttributeError this entry recorded no longer happens. Verified directly: tests/protocols/test_option_roundtrip_unit.py's httpv2-frame/SETTINGS case now returns 'OK'. This is what failed CI on Python 3.12 and Integration Python 3.15 at 0e7abbe -- not an interpreter-dependent or order-dependent failure, the same stale-entry mismatch on every interpreter; those two jobs simply reported first. Verified: mypy pcapkit -> 123 errors/40 files (unchanged). Round-trip harness, this file's own tests, and tests/protocols/internet/ test_mh_unit.py: 46 passed, 629 subtests passed, 0 failed.
main gained #461 (closing #458: prepare's @prepare decorator now distinguishes a declared zero length from a derived one, raising StreamEOFError only for the latter) since this branch's previous merge, on top of #462 (closing #459, already handled). Between the two, all three remaining httpv2-frame entries this PR's own fix had exposed -- DATA, HEADERS, CONTINUATION -- now round-trip cleanly too. Verified directly against the round-trip harness: all three return 'OK'. Rewrote the HTTP/2 section's comment block to summarise all six frames' history (PUSH_PROMISE/PING via #445 itself, DATA/HEADERS/ CONTINUATION via #461, SETTINGS via #462) now that none of them need an entry. Also merged origin/main (fa12895, #461) -- clean auto-merge, no conflicts, confirmed against #464's changes to tests/protocols/internet/test_mh_unit.py (different hunks, and its own test run clean: 62 passed, 272 subtests, 0 failed). Verified: mypy pcapkit -> 124 errors/40 files (main at fa12895: 125, one more than its previous 124 -- unrelated to this branch, and this branch stays one fewer than whatever main's own count is, from the same pre-existing type: ignore cleanup as before). Round-trip harness: 7 passed, 363 subtests passed, 0 failed. mh-extension/* re-verified 'OK' again on this merge (all four).
|
Reviewing on behalf of Copilot (out of tokens). This supersedes the stale CI
Full suite and mypy, run myself, not taken from the PR bodyGenerated fixtures with
The six
|
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.
| No dedicated class: an earlier version of this function returned a | ||
| hand-written :class:`dict` subclass, adopted when a bare | ||
| :class:`collections.ChainMap` was (wrongly) suspected of corrupting | ||
| the shared :class:`~abc.ABCMeta` cache every :class:`Schema | ||
| <pcapkit.protocols.schema.schema.Schema>` subclass used to share on | ||
| CPython <= 3.10 (issue #439). Measured after the fact: the cache keys | ||
| on the exact type queried, so asking about a :class:`~collections.ChainMap` | ||
| instance caches lookups for :class:`~collections.ChainMap`, not for | ||
| :class:`dict` -- the actual poisoning came from ordinary code asking | ||
| :func:`isinstance` about a plain :class:`dict` | ||
| (:func:`~pcapkit.corekit.infoclass.Info.__update__`), which a | ||
| :class:`~collections.ChainMap`-based context never did either way. | ||
| #439 has since been fixed directly (every :class:`Schema` subclass | ||
| now gets its own ``_abc_impl``), which removes the mechanism |
There was a problem hiding this comment.
Non-blocking observation on this paragraph (and the commit message's matching claim).
The narrow technical point -- CPython's ABCMeta cache keys on the exact type queried, so probing a ChainMap instance and probing a dict instance land in different cache slots -- checks out. I confirmed independently on this tree (Python 3.14): Schema._abc_impl is not Mapping._abc_impl (the #439 fix gives every Schema subclass its own cache), and neither issubclass(dict, Schema) nor issubclass(collections.ChainMap, Schema) is True.
But "ChainMap never poisoned anything" doesn't fully reconcile with commit 86370d7d5's own empirical result: "reverting only the ChainMap call... makes the test pass again on Python 3.10; restoring it reproduces the failure... Confirmed both in an isolated single-test run and via a local Python 3.10 venv." That was a real, directly-measured result at the time (pre-#439-fix), and nothing here explains why toggling the ChainMap call moved that specific test's outcome if constructing one were truly inert to the shared cache. A plausible reconciling story exists -- swapping ChainMap for a plain-dict-literal (or the dict subclass) changes which concrete type flows through other, unrelated isinstance calls in the same run (e.g. Info.__update__'s isinstance(dict_, (dict, Mapping))), which could shift when the pre-existing #439 corruption got triggered rather than ChainMap being causal on its own -- but that reconciliation isn't spelled out, here or in the commit message.
I can't settle this myself: this venv only has Python 3.14, and 3.14 never hit the pre-#439 code path at all (the version guard that bypassed ABCMeta.__new__ only applied below 3.11), so a clean isinstance({}, Schema) here proves nothing about the Python-3.10 claim either way -- confirming that isn't the same as confirming the historical account.
Not a blocker: #439 is now fixed at the root (independent _abc_impl per Schema subclass, already merged into this branch), so the question is moot for correctness regardless of which account is right. Flagging only because the docstring states the reversal as settled fact ("Measured after the fact") rather than as "most likely, and now unfalsifiable."
There was a problem hiding this comment.
You are right, and this is the finding I most wanted someone to make — fixed in b647e65e0.
The docstring asserted the reversal as measured fact, which it was not. What is genuinely measured is narrower than what it claimed: the ABCMeta cache keys on the exact type queried, so probing a ChainMap caches ChainMap and not dict, and #439's poisoning came from ordinary code asking isinstance about a plain dict (Info.__update__, at pcapkit/corekit/infoclass.py:270). That does not by itself explain the 3.10 observation in 86370d7d5, which was a real, directly-measured, toggled-both-ways result at the time. Your reconciliation — the ChainMap was never causal but changed which concrete types flowed through unrelated isinstance calls, and so changed when the pre-existing corruption fired — is the likeliest one, and you are right that it is plausible rather than demonstrated.
So the docstring now records it as two measurements that do not fully reconcile, says the reconciliation is plausible rather than shown, says it is no longer testable because #439's direct fix removed the mechanism, and says plainly that it does not matter for correctness either way. Your phrasing — "most likely, and now unfalsifiable" — is what it should have said in the first place.
You also correctly noted the environment limit, and I agree it is decisive: only the 3.14 venv has dependencies installed, and 3.14 never took the pre-#439 code path at all, since the version guard that bypassed ABCMeta.__new__ only applied below 3.11. A clean isinstance({}, Schema) here proves nothing about the 3.10 claim. Building a 3.10.21 venv from scratch would be the only way to settle it, and I have deliberately not done that: with the mechanism removed at the root, the answer changes no code.
This one is worth more than a docstring tidy-up, which is why it got a commit of its own rather than being folded in: repeating an unverified conclusion as settled fact is a mistake this branch has already made twice, and a docstring is exactly where it would have outlived the PR and been cited later as evidence.
One thing your finding did not reach, which I fixed alongside: tests/corekit/test_fields_misc_packet_context.py's own class docstring carried the same "(wrongly) suspected" claim, so it would have been left contradicting the module it documents. Softened to match, with a pointer to the function's fuller account. There is no wrongly left in either file.
No executable line changed — verified by filtering the diff — and tests/corekit/test_fields_misc_packet_context.py plus tests/protocols/internet/test_mh_unit.py are 40 passed, 272 subtests, 0 failed.
There was a problem hiding this comment.
You were right, and this is fixed in b647e65e0 — the docstring now records the two measurements as not fully reconciling rather than as a settled reversal.
I went and read 86370d7d5 rather than arguing from memory, and it is as strong as you say:
Measured directly: with everything else unchanged, reverting only the ChainMap call back to a plain
{'__packet__': packet}literal makes that test pass again on Python 3.10; restoring it reproduces the failure. Confirmed both in an isolated single-test run and via a local Python 3.10 venv.
So the docstring was asserting a reversal of a directly-measured result on the strength of a narrower measurement that does not explain it. Your reconciliation — the ChainMap was never causal but changed which concrete types flowed through unrelated isinstance calls, and so changed when the pre-existing corruption fired — is the one I find likeliest too, and the docstring now says exactly that, labelled as plausible rather than demonstrated.
I also agree it is no longer decidable, and for the reason you gave: #439's direct fix removed the mechanism, so the original conditions are gone. 3.10 interpreters do exist on this machine (/home/jarryx/.local/share/mise/installs/python/3.10.21, which is CI's exact patch level) and a venv against one was built earlier in this programme, so it is not the interpreter that is missing — it is that answering the question now would require also reverting #471 to restore the corruptible cache, i.e. reconstructing a state the codebase has deliberately left behind. Not worth it for a question that does not affect correctness either way.
The change is prose only. Verified mechanically rather than by eye: parsing misc.py before and after and comparing the ASTs with docstrings stripped gives an exact match, so no executable line moved.
Thank you for pushing on this one. Repeating an unverified conclusion as fact is a mistake this branch has already made twice, and a docstring is precisely where it would have outlived the PR.
| HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) | ||
|
|
||
|
|
||
| class NestedPacketContextTests(unittest.TestCase): |
There was a problem hiding this comment.
Non-blocking nit. NestedPacketContextTests names the deleted NestedPacketContext class, not the nested_packet_context() function that replaced it -- a dangling reference to a class that no longer exists in pcapkit/corekit/fields/misc.py.
The class's own docstring immediately below explains the history in detail ("An intermediate version of this fix used a hand-written dict subclass..."), so this reads as a deliberate, self-documented choice rather than an oversight, and I wouldn't block on it. Still, a name like NestedPacketContextSemanticsTests would describe what the class actually tests today (the ChainMap-based fallback semantics) rather than an implementation that was tried and retired within this same PR's history. Optional.
There was a problem hiding this comment.
Taken — renamed to NestedPacketContextSemanticsTests in b647e65e0, using your suggestion verbatim.
You read it more carefully than I did. I had looked at this earlier and talked myself out of it on the grounds that the name refers to the concept rather than the deleted class, so it was harmless. That was wrong for the reason you give: the class docstring immediately below spends a paragraph on the retired dict subclass, so the name does read as pointing at that class rather than at nested_packet_context(). A name that needs the docstring below it to disambiguate is not doing its job.
grep -rn NestedPacketContextTests pcapkit tests docs returns nothing after the rename, so there was no other reference to update.
There was a problem hiding this comment.
Agreed and done in b647e65e0 — renamed to NestedPacketContextSemanticsTests, which is the name you suggested and describes what the class actually covers.
You were right that it was a dangling reference rather than merely an odd name: NestedPacketContext was deleted earlier in this same branch, so nothing in pcapkit, tests or docs defines it any more. Confirmed with a grep across all three after the rename — the only remaining occurrences of the string are inside the new class name and the prose that explains the history.
While there I also softened that class's own docstring, which carried the same "(wrongly) suspected" overclaim as the module docstring you flagged in the other thread, so the two no longer contradict each other.
Review of PR #457 (
|
Both changes are docstring prose plus one test class name. No executable line changes; nothing about the fix itself moves. The review of 82dbf94 was right that nested_packet_context's docstring overclaimed. It stated the ChainMap-poisoning reversal as measured fact, which does not reconcile with commit 86370d7's own directly-measured result: on a real CPython 3.10 venv at the time, toggling only the ChainMap call moved test_pcapng_remaining_constructor_branches_and_custom_dispatch between passing and failing, both ways. What is genuinely measured is narrower: the ABCMeta cache keys on the exact type queried, so probing a ChainMap caches ChainMap and not dict, and #439's poisoning came from ordinary code asking isinstance about a plain dict (Info.__update__). That does not by itself explain the 3.10 observation. The likeliest reconciliation -- the ChainMap was never causal but changed which concrete types flowed through unrelated isinstance calls, and so changed when the pre-existing corruption fired -- is plausible rather than demonstrated, and is now untestable: #439's direct fix removed the mechanism, so the original conditions are gone. The docstring now records that as two measurements that do not fully reconcile, and says plainly that it does not matter for correctness either way. This matters beyond tidiness: repeating an unverified conclusion as fact is a mistake this branch has already made twice, and a docstring is where it would have outlived the PR. Also, since the test file's own class docstring carried the same "(wrongly) suspected" claim, it is softened to match rather than left contradicting the module it documents. NestedPacketContextTests -> NestedPacketContextSemanticsTests: the old name referred to a class deleted earlier in this same branch, so it was a dangling reference; the new one names what the tests actually cover, the ChainMap-based fallback semantics. No other reference to the old name exists in pcapkit, tests or docs. Verified: tests/corekit/test_fields_misc_packet_context.py plus tests/protocols/internet/test_mh_unit.py -> 40 passed, 272 subtests, 0 failed. The diff touches no executable line in misc.py.
Re-review at
|
|
Both of the things you declined to confirm deserve an answer on the record, since you were right to flag them rather than assume. The A figure quoted without its selection is not a measurement anyone else can check, which is the whole point of quoting it. My fault, not a bad number. On the backticks you are simply right, and my brief was wrong. I told you two single-backtick I did make that edit, but to an uncommitted intermediate state of the file that never existed at Your third caveat — the Sphinx build timing out at 170s on Two things in your report I have adopted:
|
…ap (#445) The owner picked the flat dict after being shown the comparison. This is the third container this function has returned -- ChainMap, then a hand-written dict subclass, then ChainMap again -- and a plain dict is what ends the question rather than continuing it. return {**packet, '__packet__': packet} What it buys, measured rather than argued: - The cast at SchemaField.pack's call site is gone. A real dict satisfies Schema.pack's own dict[str, Any] annotation natively, so there is nothing left to assert. That cast was the one wart of the ChainMap version and the thing its review probed hardest. - mypy is unchanged: 124 errors either way, and the only diff in the full error lists is the same four pre-existing misc.py errors shifted one line, because dropping the now-dead `import collections` removed a line. No error added, none removed. - Every mapping operation is dict's own, so setdefault, pop, ==, iteration and dict(**pkt) need no override and behave the way a reader of the code expects. The ChainMap version got setdefault and __eq__ right too, but by delegation rather than by being the thing itself. - It cannot interact with ABCMeta at all, dict not being an ABCMeta-based class, which retires the #439 question for this function permanently instead of leaving it resting on an argument about cache keying. Two consequences of a copy rather than a live view, both deliberate and neither reached by any current call site, now documented: deleting a name from the context removes it outright rather than reverting to the enclosing schema's value, and a mutation of the enclosing packet made after the context is built is not observed through it -- __packet__ stays bound to the live enclosing mapping for any callback that needs current values. The docstring is rewritten accordingly, and now records the CGAExtension case concretely -- it declares its own `length` while the option enclosing it declares `length` too -- so the reason the context must be write-local is a real collision in this codebase rather than a hypothetical. Verified on the merged tree (origin/main f7b5cc5 merged in first, clean): tests/corekit/test_fields_misc_packet_context.py + tests/protocols/schema/ + tests/protocols/misc/test_pcapng_unit.py + tests/protocols/internet/test_mh_unit.py -> 101 passed, 416 subtests, 0 failed, identical to the ChainMap version. All ten EXPECTED_FAILURES labels deleted by this branch still return OK, enumerated individually through options.cases() rather than inferred from a green suite.
…Map (#445, #474) Docstring only. Caught by grepping for leftover references after #457 merged: the test class's own docstring still said nested_packet_context "replaces that literal with a two-level collections.ChainMap", which stopped being true when that PR switched to a plain dict. I rewrote the function's docstring in misc.py and missed this one, so main shipped with the two contradicting each other. It now describes the copy, and says what the plain dict settles that neither predecessor did: the retired dict subclass's setdefault bypassed the fallback and its __eq__ compared only its own storage, while the ChainMap got both right by delegation but needed a cast at the call site, not satisfying Schema.pack's dict[str, Any] annotation. A copy needs neither a cast nor an override. Also names #474 explicitly, since that issue is exactly the setdefault/__eq__ gap this docstring pins. tests/corekit/test_fields_misc_packet_context.py: 4 passed.
Three factual errors in test prose, found while surveying #NNN citations under tests/ for #719: - test_http_unit.py:2356-2365 claimed PR #457 was "still-open" and that the SETTINGS round trip "remains unreachable until that lands". Both were stale: #457 merged 2026-09-18, the `httpv2-frame/SETTINGS` key no longer exists in `EXPECTED_FAILURES` (verified by importing it: 43 keys, only httpv2 key is PRIORITY), and the round trip itself passes (reproduced: `httpv2.roundtrip()` reports 'OK' for that case). Rewrote the paragraph: the round trip works; what still raises `KeyError: 'flags'` is a bare `SettingsFrame(...).pack()` with no enclosing packet (reproduced directly), which is an unsupported invocation, not a round-trip defect. Cited `FrameType.post_process` by name rather than the stale `httpv2.py:144` line number (the real raise is at `packet['flags'][name]`, confirmed by traceback). `GH-445` is left alone -- it is this repo's own issue shorthand, used throughout pcapkit/ and tests/, and #445 is in fact an issue. - test_base_class_contract.py:45 called #547 and #570 "issues"; both are pull requests. - test_const_str_payload_870_unit.py:12 called #869 a "GitHub issue"; it is a pull request. No behaviour or citation-style changes -- tests/** is exempt from the #719 PR-citation rule. Ran each file individually under pytest and plain unittest (all three use subTest): 6/6, 7/7, 60/60 passed both ways.
Three factual errors in test prose, found while surveying #NNN citations under tests/ for #719: - test_http_unit.py:2356-2365 claimed PR #457 was "still-open" and that the SETTINGS round trip "remains unreachable until that lands". Both were stale: #457 merged 2026-09-18, the `httpv2-frame/SETTINGS` key no longer exists in `EXPECTED_FAILURES` (verified by importing it: 43 keys, only httpv2 key is PRIORITY), and the round trip itself passes (reproduced: `httpv2.roundtrip()` reports 'OK' for that case). Rewrote the paragraph: the round trip works; what still raises `KeyError: 'flags'` is a bare `SettingsFrame(...).pack()` with no enclosing packet (reproduced directly), which is an unsupported invocation, not a round-trip defect. Cited `FrameType.post_process` by name rather than the stale `httpv2.py:144` line number (the real raise is at `packet['flags'][name]`, confirmed by traceback). `GH-445` is left alone -- it is this repo's own issue shorthand, used throughout pcapkit/ and tests/, and #445 is in fact an issue. - test_base_class_contract.py:45 called #547 and #570 "issues"; both are pull requests. - test_const_str_payload_870_unit.py:12 called #869 a "GitHub issue"; it is a pull request. No behaviour or citation-style changes -- tests/** is exempt from the #719 PR-citation rule. Ran each file individually under pytest and plain unittest (all three use subTest): 6/6, 7/7, 60/60 passed both ways.
Summary
Closes #445.
A nested schema's field callbacks got a packet dict holding the enclosing
schema only under
__packet__, so a callback written the ordinary way --length=lambda pkt: pkt['length'], exactly as every top-level schema writesit -- raised
KeyErrorthe moment the schema was nested. The measuredcasualty is CGA Parameters:
CGAParameter.extensions(
pcapkit/protocols/schema/internet/mh.py:515-521) sizes itself frompkt['length'], which belongs to the enclosingCGAParametersOption, and awell-formed 40-octet option raised
KeyError: 'length'inSchemaField.unpack(pcapkit/corekit/fields/misc.py:619, the{'__packet__': packet}literal).Design chosen: chained lookup (option 1 of the three in the issue)
nested_packet_context()(pcapkit/corekit/fields/misc.py) replaces theliteral with a
NestedPacketContext, adictsubclass (see "A Python3.10-only regression" below for why it is a
dictsubclass rather thancollections.ChainMap, which is where this started). A name the nestedschema does not declare falls through to the enclosing schema; a name it
does declare, or
__packet__itself, is found locally first. This designwas chosen over the other two because:
callback site, including ones outside this PR's scope, to get the same
fix
mh.py's CGA Parameters needed. The chained lookup needed zerochanges to
mh.py:CGAParameter.extensions's existingpkt['length']just resolves once the mapping falls through. Verifiedby reproducing the issue's exact 40-octet packet before and after.
names: it lets an inner field silently shadow an outer one. The mapping
keeps them apart -- see the shadowing test below.
__packet__contract had exactly one piece of documentation (adocstring on an unrelated pcapng.py helper) and zero mentions in
Schema.unpack's own reserved-key list. Both are fixed: the contract isnow documented on
nested_packet_contextitself, andSchema.unpack'sdocstring names
__packet__alongside__length__and__option_padding__.Write path (explicitly checked, since a mapping that writes through or
drops writes would be worse than today): plain
dictassignment anddeletion (
pkt[key] = value,del pkt[key]) always act on this instance'sown storage -- that is what a
dictsubclass gives for free, with nooverride needed. So a field a nested schema sets -- including one that
shadows a parent field name -- is never visible to the parent, and never
silently dropped either.
inand.get()are overridden (thedictbuilt-ins for both bypass
__missing__and would otherwise miss thefallback entirely), and so are
__iter__/__len__/keys/values/items,for the union-of-both-levels iteration the design promises. All of this is
covered by
test_nested_schema_reads_enclosing_field_by_name_and_does_not_leak_writes.pcapkit/protocols/schema/misc/pcapng.py's two existing__packet__consumers (
packet_byteorder,BlockType.post_process) are untouched.Both are called directly, in tests and in
pcapkit/foundation/engines/pcapng.py:192, with a plain{'__packet__': {...}}dict rather than throughSchemaField, so theirhand-rolled fallback has to keep handling that shape regardless of this
design -- simplifying them to rely on the chain would have broken those
direct callers. Covered by
test_pcapng_byteorder_consumer_still_works_with_both_shapesandtest_pcapng_block_type_mismatch_consumer_still_works_with_both_shapes,each exercising both the hand-built dict and
NestedPacketContext.A Python 3.10-only regression, and why
NestedPacketContextis adictsubclassThe first pushed version of this PR used
collections.ChainMap({'__packet__': packet}, packet)directly, and CI went red on exactly two legs:Python 3.10andIntegration Python 3.10, both ontests/protocols/misc/test_pcapng_unit.py::PCAPNGUnitTests::test_pcapng_remaining_constructor_branches_and_custom_dispatch,with
AttributeError: 'dict' object has no attribute 'to_dict'fromSchema.to_dict(schema.py:520,isinstance(value, Schema)wrongly truefor a plain
dict).That test has nothing to do with CGA Parameters, HTTP/2, or MH -- it
exercises fifteen unrelated PCAP-NG option constructors. The actual cause,
confirmed with a local Python 3.10 venv:
isolation (no other test file loaded, so none of this PR's new dynamic
Schemasubclasses are even created) as soon as this PR'smisc.pychange alone is applied.
collections.ChainMap(...)call back to a plain{'__packet__': packet}literal -- nothing else changed -- makes it passagain. Restoring the
ChainMapcall reproduces the failure.Diagnosis (full version posted on #439):
Schemainheritscollections.abc.Mapping(schema.py:245), and on CPython <= 3.10SchemaMeta.__new__bypassesABCMeta.__new__, so noSchemasubclassever gets its own
_abc_impl-- every one of them sharesSchema's.Asking
Mappinga question before askingSchemaone therefore poisonsthe cache for the whole family.
collections.ChainMapis itself acollections.abc.MutableMapping, and constructing one pulledMappinginto the question order, flipping the unrelated, later
isinstance(some_dict, Schema)check in the PCAP-NG test fromFalsetoTrue. This is not a fault in the chained-lookup design; it is #439 (notthis PR's to fix) surfacing through an implementation detail of this PR's
own code.
Because the failing test is a plain
unittestmethod, not one oftest_option_roundtrip_unit.py's per-code cases,INTERPRETER_GAPScannotexpress it -- that table only overrides a
case.labellookup, and there isno table to add this to. Rather than leave a real, reproducible CI failure
in place,
NestedPacketContextnow subclassesdictdirectly instead ofChainMap/Mapping.dict's own metaclass is plaintype, notABCMeta-- constructing or using adictsubclass never asksMappinganything, so the shared cache is never touched. Being a real
dictalsonominally satisfies every existing
packet: 'dict[str, Any]'annotation onthe rest of the field classes, so no other file needed to change (see the
mypy section below for why that mattered).
__missing__gives the enclosing-schema fallback for free:dict's own__getitem__calls it automatically the moment a key is not foundlocally.
__contains__andgetare overridden, since thedictbuilt-ins forboth bypass
__missing__entirely and would otherwise never fallthrough.
__iter__/__len__/keys/values/itemsare overridden for theunion-of-both-levels iteration the design promises;
dict's own versionswould only see this instance's local keys.
copyis overridden becausedict.copy()always returns a plaindict,even for a subclass, which would silently drop the fallback.
dict's own__setitem__/__delitem__already only touch this instance's ownstorage, which is exactly the write isolation the design requires.
Same fallback, same
__packet__reachability, same write isolation as theChainMapversion -- confirmed by the unchanged test suite (allpre-existing tests pass unmodified). Verified after the change: the Python
3.10 venv runs
tests/protocols/misc/test_pcapng_unit.pyclean (37 passed,139 subtests, 0 failed), and the previously-failing test passes both alone
and as part of that module.
mypy: two errors, from the first
NestedPacketContext, now zero netAn intermediate version of this fix (after moving off
ChainMap, beforesettling on
dict) implemented the same two-level mapping from a bareobject deriving from nothing at all. That is a legitimate way to avoid
collections.abcentirely, but it does not satisfy the existingpacket: 'dict[str, Any]'annotationSchema.pack/unpackdeclare, somypy flagged
Argument 1 to "pack" of "Schema" has incompatible type "NestedPacketContext"; expected "dict[str, Any] | None", plus anSchemaField.lengthtype: ignore[has-type]that the restructuring madenewly unused.
Widening
Schema.pack/unpack's annotation to admit the new type directlywas the obvious fix and the wrong one:
packetflows from there intopre_unpack/pre_pack/post_process,FieldBase.__call__,ListField.pack,ConditionalField.testand more, each with its ownnarrowly-typed
packet: 'dict[str, Any]'signature. Widening only the twoSchemamethods produced seven new errors at those call sites; doing itproperly would mean widening every field class's own signature, well
outside this PR's file list.
Making
NestedPacketContextadictsubclass (previous section) sidestepsthis too: it satisfies the existing annotation nominally, everywhere,
because it is one. The
type: ignore[has-type]was simply deleted ratherthan replaced -- and checked: it is already flagged unused on a clean
da2422728checkout (no changes at all), so it is a pre-existing,unrelated mypy hygiene gap this restructuring happened to touch, not
something this PR introduced.
mypy pcapkiton this PR's head: 123errors in 40 files (
main: 124) -- one fewer than baseline, and no newerror anywhere.
One stale citation
The
httpv2-frame/{DATA,HEADERS,CONTINUATION}Gap entries'defectstringpointed at
pcapkit/utilities/decorators.py:222; the actualraise EOFErroris at:228(prepareitself starts at:177). Corrected.#437's three
Exp_FFFD/Exp_FFFE/Exp_FFFFfailuresYes, same defect -- and #437 has since merged (
489eef651), which let thisbe checked directly rather than by reading its diff.
ExperimentalExtensiondeclares
data: 'bytes' = BytesField(length=lambda pkt: pkt['length']), butlengththere isCGAExtension's own field (parsed locally, no nestingproblem). The
KeyErrorthose three cases hit happened beforeExperimentalExtensionwas even selected:CGAParameter.extensions'sOptionFieldhas to size the whole extensions area first, via the samepkt['length']this PR fixes, regardless of which extension type ends upinside it. #437 itself registered all three codes and attributed all four
mh-extension/*entries (includingMulti_Prefix) to #445 with exactlythat reasoning. See "Second merge" below for what happens to those four
entries once this branch also carries #437 and #446/#456.
CGA Parameters:
#446merged too, so it now parses end to endThis PR alone does not unblock CGA Parameters: with only #445 fixed, it
reaches
FieldValueError: Field parameters has invalid length.-- theseparate, already-filed
ForwardMatchField/Schema.__len__defect (#446)-- instead of the
KeyError. That was true when this PR was first opened.#446 has since merged as #456 (
0283a6d59), and with both fixes on thesame tree, the issue's own 40-octet reproduction parses completely: no
exception, a populated
CGAParametersOptionwith oneCGAParameter. See"Second merge" below for the test updates this required.
EXPECTED_FAILURESfallout in the round-trip harnessFixing the
KeyErrorunblocks the same latent defect in six HTTP/2 frameschemas (they read the header's
flagsthe same way, on the pack side).Running
tests/protocols/test_option_roundtrip_unit.pyafter the fixturned seven cases red against the old table, each hitting a distinct,
unrelated, previously-unreachable defect underneath:
httpv2-frame/PUSH_PROMISE,httpv2-frame/PING: now round-trip cleanly-- entries deleted.
httpv2-frame/DATA,HEADERS,CONTINUATION: nowEOFError--pcapkit/utilities/decorators.py:222'sprepareunconditionally treatsa zero-length nested unpack as end-of-file, which is wrong for a frame
that legitimately has no payload. Not fixed here (that file is owned by
open PR utilities: let @prepare read length/packet by keyword, not position #450).
httpv2-frame/SETTINGS: nowAttributeError: 'SettingPair' object has no attribute 'length'--SettingsFrame.settings(
pcapkit/protocols/schema/application/httpv2.py:283-285) passes theSettingPairclass toListField'sitem_typeinstead of wrapping itin a
SchemaField, silenced by a# type: ignore[arg-type]. Filed asSettingsFrame.settings passes a Schema class to ListField's item_type instead of a SchemaField #459; not fixed here.
mh-extension/Multi_Prefix: at the time, nowFieldValueError: Field prefixes has invalid length--_make_ext_multiprefix(
pcapkit/protocols/internet/mh.py:3864) wrotelength=1 + len(prefixes) * 16(17 octets for one prefix) instead of4 + len(prefixes) * 8(12). Not a new defect: open PR protocols: complete the Mobility Header registry -- all 24 message types, 70 of 71 options, all 4 CGA extensions #437 alreadyfixed exactly this. protocols: complete the Mobility Header registry -- all 24 message types, 70 of 71 options, all 4 CGA extensions #437 has since merged -- see "Second merge" below for
where this entry ends up.
Two more defects surfaced the same way, filed rather than fixed here:
#458 (
@prepareraises a bareEOFErrorfor any zero-length schema,decorators.py:227-228-- the mechanism behind theDATA/HEADERS/CONTINUATIONentries above) and #459 (above). Both were open whenthis was written and have since merged -- see "Third merge" below.
All entries re-attributed with the new status, fragment and
file:line,verified against the actual exception text.
Second merge:
maingained #437 and #446/#456 mid-reviewmainmoved again while this PR was in review -- #437 (MH registrycompletion) and #456 (issue #446, the
ForwardMatchFielddouble-count)both merged. Merged
origin/main(0283a6d59) a second time, oneconflict in the same
mh-extensionregion ofEXPECTED_FAILURES(resolved by re-deriving each entry from the actual post-merge behaviour,
not by picking a side):
mh-extension/{Exp_FFFD,Exp_FFFE,Exp_FFFF}-- alongsideMulti_Prefix,all four attributed to A nested schema cannot reach the enclosing packet's fields by name, so CGA Parameters raises KeyError: 'length' #445 with the identical
KeyError: 'length'.Ran each of the four individually against the merged tree rather than
assuming they behave alike: all four now return
'OK'. protocols: complete the Mobility Header registry -- all 24 message types, 70 of 71 options, all 4 CGA extensions #437 bothregistered the three experimental codes and fixed
_make_ext_multiprefix's bogus arithmetic; schema: a forward match consumes nothing, so bill it nothing in len(schema) #456 fixed theForwardMatchFielddouble-count that stoppedCGAParametersOption.parametersfrom sizing correctly. Combined withthis PR's own fix, nothing blocks any of the four any more. All four
entries deleted.
above).
test_cga_parameters_option_reaches_the_446_boundary_not_a_keyerror,which asserted the now-stale
FieldValueErrorboundary, is rewritten astest_cga_parameters_option_now_parses_end_to_end, asserting the parsedfields directly.
KeyErrorastests/protocols/internet/test_mh_unit.py::test_mh_cga_parameters_option_is_unparsable_upstream,explicitly so that "whoever fixes it finds out here" -- and it did: that
test went red the moment this merge landed, for exactly the reason its
own docstring named. Replaced it with
test_mh_cga_parameters_option_now_parses, asserting the option parseswith the fields the wire says it should have, and corrected the now-false
claim in
test_mh_pmipv6_options_round_trip_byte_for_byte's docstring(CGA_Parameters is still excluded from that test's round-trip cases, but
no longer because it cannot be parsed -- that is a separate, deliberate
scope decision for whoever adds it, not implied by parsing alone).
Re-verified after this second merge:
mypy pcapkit-> 123 errors/40files (a fresh
origin/mainat0283a6d59: 124, unchanged from thefirst merge's baseline). Round-trip harness: 7 passed, 363 subtests
passed, 0 failed (up from 299 before #437 grew the
mh-extensionfamilyto four codes).
tests/protocols/internet/test_mh_unit.py: 35 passed, 266subtests passed, 0 failed. Full suite (Python 3.14,
PYTHONSAFEPATH=1 python -m pytest tests -q): 1001 passed, 17 skipped,1547 subtests passed, 0 failed (883s).
NestedPacketContext: from a hand-written mapping to adictsubclass, and back to semanticsThe
ChainMap-vs-dictdiagnosis above turned out to be only half right onreview.
#462merged mid-review and, independently, an owner review threadasked why
nested_packet_context()doesn't just reuseInfo(
pcapkit/corekit/infoclass.py) instead of a dedicated class. Rather thanargue from the (now-corrected) ABC-cache mechanism, I built an actual
Info-based nested context on disk and ran it: on a local Python 3.10.21venv (matching CI's exact patch),
tests/protocols/misc/test_pcapng_unit.py-- the module that broke under
ChainMap-- came back clean, 37 passed,139 subtests, 0 failed. So
Infois not unsafe here; the real question issemantics, not the ABC cache:
__missing__-style fall-through: no saving either way.Info.__getitem__is a from-scratch
self.__dict__[...]lookup with no such hook (that's adictC-level feature), so it needs the same custom code whichever baseis used.
__contains__/.get(): a genuine point forInfo--Mappingsuppliesmixins for both that delegate to
__getitem__, so fixing__getitem__once gets both for free, where
dict's own versions bypass__missing__and need separate overrides.
Infoisdeliberately immutable (
__setattr__raises, no__setitem__at all),but field callbacks write into the packet dict throughout parsing
(
Schema.unpack's per-field loop:packet[field.name] = value, once perfield). Supporting that on an
Infosubclass means writing intoself.__dict__directly from a custom__setitem__, bypassing ratherthan extending the immutability
Info's own docstring promises.Infoinstances carry__map__/__map_reverse__inself.__dict__(for its builtin-name-collisionhandling), which then show up in naive iteration alongside the real
packet keys -- measured directly, not assumed.
Full reasoning posted on the owner's thread
(#457 (comment));
left unresolved for the owner to close.
Third merge:
maingained #461 (closing #458) and #462 (closing #459)Two more of the newly-surfaced defects this PR had filed (#458, #459) were
fixed and merged while this was in review, as
#461and#462. Mergedorigin/main(fa128959e, thene7004191aafter reconciling with aduplicate parallel merge already pushed to this branch) -- no conflicts;
#464's changes totests/protocols/internet/test_mh_unit.pyland in adifferent region and were confirmed non-interacting by running that file
(62 passed, 272 subtests, 0 failed).
#461makes@preparedistinguish a declared zero length (a nestedschema legitimately sized to zero) from a derived one (genuine
end-of-stream), raising only for the latter. Verified directly:
httpv2-frame/{DATA,HEADERS,CONTINUATION}all now return'OK'.#462wrapsSettingsFrame.settings's item type inSchemaField(schema=SettingPair). Verified directly:httpv2-frame/SETTINGSnow returns'OK'too.All six
httpv2-frameentries this PR's own fix had exposed are now gone:PUSH_PROMISE/PINGclosed by #445 itself,DATA/HEADERS/CONTINUATIONby #461,
SETTINGSby #462. Rewrote the section comment to summarise allsix rather than describe five stale gaps.
Final numbers, at this branch's current head:
mypy pcapkit-> 124errors/40 files (a fresh
origin/mainatfa128959e: 125 -- one morethan its own earlier count, unrelated to this branch, and this branch
stays one fewer than whatever
main's own count is, from the samepre-existing
type: ignorecleanup as before). Round-trip harness: 7passed, 363 subtests, 0 failed. Full suite (Python 3.14): 1010 passed,
17 skipped, 1553 subtests passed, 0 failed (906s). CI is green: all
22 checks pass (2 skip by design -- the docs gate and the single-version
full-suite gate).
Testing
New file
tests/corekit/test_fields_misc_packet_context.py:test_nested_schema_reads_enclosing_field_by_name_and_does_not_leak_writes-- the load-bearing case. Before:
KeyError: 'length'(matches theissue exactly). After: passes, and also checks shadowing, the write
path,
in,.get()and iteration in one pass.test_pcapng_byteorder_consumer_still_works_with_both_shapes,test_pcapng_block_type_mismatch_consumer_still_works_with_both_shapes-- the two existing consumers, with a hand-built dict and with the new
chain. Before:
ImportError(the helper does not exist yet).After: pass.
test_cga_parameters_option_reaches_the_446_boundary_not_a_keyerror--the issue's own 40-octet reproduction. Before:
KeyError: 'length'at
mh.py:516. After:FieldValueError, confirming the fix workedand CGA Parameters correctly still does not parse.
Baseline at
e2d8ed6d1(this branch's original merge-base, confirmed bytemporarily reverting the changed files back to that commit's content in
this worktree, not quoted second-hand):
PYTHONSAFEPATH=1 python -m pytest tests -q-> 960 passed, 17 skipped, 1264 subtests passed (617s). Afterthis PR's original commit, same command: 964 passed, 17 skipped, 1264
subtests passed, 0 failed (605s).
mainthen moved four commits (#449, #450, #451, #453) while this PR was inreview, so it was merged (
git merge --no-ff origin/main, one cleanauto-merge in the
EXPECTED_FAILUREStable -- see the heads-up above) andre-baselined the same way, at the new merge-base
da2422728: 977 passed,17 skipped, 1267 subtests passed (609s). After this PR's changes on top
(including the
NestedPacketContextfix described above): 981 passed, 17skipped, 1267 subtests passed, 0 failed (638s) -- the same 4 new tests
join the passing count; the subtest total is unchanged from the new
baseline (main's own commits added tests of their own, which is why 1267
differs from the original 1264).
Also verified on a local Python 3.10 venv (this repo's CI runs 3.10-3.15;
only 3.10 carries #439's risk): 943 passed, 55 skipped, 1237 subtests
passed, 0 failed -- the higher skip count is only this venv missing some
optional runtime deps (dpkt/pyshark extras), not a difference in outcome.
Test plan
pcapng.py__packet__consumers verified unaffectedFieldValueError(ForwardMatchField's non-consuming bytes count toward Schema.__len__, so correct input fails a declared-length check #446), not theKeyError#437's threeExp_FFF*failures confirmed to be this same defecttests/protocols/test_option_roundtrip_unit.py) green,EXPECTED_FAILURESupdated for all 7 cases whose status changedtest_pcapng_unit.py, and a local full-suite run) confirmed clean after moving offChainMapmypy pcapkitconfirmed at 123 errors/40 files on this PR's head, against 124 onmain-- no new error, one pre-existing one incidentally cleaned upRevision at
82dbf9416— the dedicated class is goneAppended rather than edited in, because the review comments above quote the original text.
At the repo owner's request,
NestedPacketContextis deleted.nested_packet_context()now returns a barecollections.ChainMap({'__packet__': packet}, packet)— no dedicated class, which is where this design started beforeChainMapwas suspected of disturbing #439.That suspicion was wrong, and the retired class's docstring asserted it at length, so it is worth stating plainly: the ABCMeta caches key on the exact type queried, not on ancestry. Asking about a
ChainMapinstance cachesChainMap, notdict. The actual poisoner was ordinary code askingisinstanceabout a plaindict—pcapkit/corekit/infoclass.py:270doesisinstance(dict_, (dict, collections.abc.Mapping))— which aChainMap-based context never did either way. #471 has since fixed #439 directly by giving everySchemasubclass its own_abc_impl, removing the mechanism regardless.What dropping the class buys, with no code to maintain — each measured on a fresh context, since these mutate and a reused one contaminates the next reading:
dictsubclassChainMapsetdefault('length', 999), parent has3999— bypassed__missing__, inserted locally3ctx == dict(ctx)False—dict.__eq__saw only__packet__Truepop('length')KeyErrorKeyError "Key not found in the first mapping: 'length'"— an explicit refusalcopy()The first three rows are the substance of #474, which this closes out.
Write isolation is unchanged:
ctx['length'] = 99leaves the enclosing dict at3,ctx['newname'] = 5never appears in it, anddel ctx['length']falls back to3rather than deleting the parent's.One thing needed a local fix rather than a class. A
ChainMapis not nominally adict, so it does not satisfySchema.pack'sdict[str, Any]annotation. Widening that annotation cascades into every other field class'spack/unpack, which forward the same argument onward with their owndict[str, Any]annotations (measured: +7 new mypy errors). There is a single explicitcast('dict[str, Any]', …)at the one call site instead, asserting thatChainMapsupports every operation the annotation promises — subscript,in,.get(), iteration.Still required alongside #471, which is the obvious question and now measurable rather than arguable, since
maincontains #471 as5182ad0ce: #471's merge touches no line ofpcapkit/corekit/fields/misc.py, and reverting only that file toorigin/main's version — leaving all of #471 in place — fails 4 tests, includingCGAParametersRegressionTests::test_cga_parameters_option_now_parses_end_to_end, the #445 symptom itself. Restore it and the same selection is 101 passed, 416 subtests, 0 failed.The ten
EXPECTED_FAILURESdeletions were re-verified individually, not inferred from a green suite — "suite green" and "case covered" are different claims.cases()fromexamples/generators/options.pyenumerates 322 cases; all ten labels are present in that enumeration (sixhttpv2-frame:CONTINUATION,DATA,HEADERS,PING,PUSH_PROMISE,SETTINGS; fourmh-extension:Exp_FFFD,Exp_FFFE,Exp_FFFF,Multi_Prefix), androundtrip()returnsOKfor each. 87 entries onmain, 77 here.Two corrections at
b647e65e0The revert-proof figure above is wrong: it fails 5 tests, not 4. My original run reverted
misc.pybut then executed onlytests/corekit/test_fields_misc_packet_context.py, so it never reached the fifth —tests/protocols/internet/test_mh_unit.py::MHUnitTests::test_mh_cga_parameters_option_now_parses, a second and independent witness to the #445 symptom in a different file. Across the full targeted selection it is5 failed, 96 passed, 416 subtests; restoring the file gives101 passed, 416 subtests, 0 failed. The error understated the evidence that this fix is load-bearing.The
nested_packet_contextdocstring overclaimed the #439 history, and no longer does. It stated the "ChainMap never poisoned anything" reversal as measured fact, which does not reconcile with commit86370d7d5's own directly-measured result — on a real CPython 3.10 venv at the time, toggling only theChainMapcall movedtest_pcapng_remaining_constructor_branches_and_custom_dispatchbetween passing and failing, both ways. What is genuinely measured is narrower: the ABCMeta cache keys on the exact type queried, and #439's poisoning came from ordinary code askingisinstanceabout a plaindict(Info.__update__). The likeliest reconciliation — theChainMapchanged which concrete types flowed through unrelatedisinstancecalls and so changed when the pre-existing corruption fired — is plausible rather than demonstrated, and is now untestable, since #439's direct fix removed the mechanism. The docstring records that as two measurements that do not fully reconcile, and says it does not affect correctness either way.Also
NestedPacketContextTests→NestedPacketContextSemanticsTests: the old name referred to a class deleted earlier in this same branch.Both changes are prose plus one class name. Verified mechanically rather than by eye — parsing
misc.pybefore and after and comparing ASTs with docstrings stripped gives an exact match, so no executable line moved, and theGOOD TO MERGEat82dbf9416stands by content.