Skip to content

NestedPacketContext: setdefault, pop and __eq__ do not honour the enclosing-schema fallback that [], in and get() do #474

Description

@JarryShaw

Summary

NestedPacketContext (pcapkit/corekit/fields/misc.py:493, added by #457)
overrides __getitem__ via __missing__, plus __contains__ and get, so a
name the nested schema does not declare falls through to the enclosing schema.
Three other dict methods were not overridden and do not honour that
fallback, so they disagree with [], in and .get() on the same key.

All three are latent — nothing under pcapkit/ calls setdefault, pop or
== on a packet context today (grep-confirmed). Filing so the inconsistency is
recorded rather than rediscovered by whoever first reaches for one of them.

Measured

On #457's head aab958b1d, with a context whose enclosing packet is
{'length': 42, 'other': 7} — each on a fresh context, since these
operations mutate:

ctx['length']                 -> 42          (fallback honoured)
'length' in ctx               -> True        (fallback honoured)
ctx.get('length')             -> 42          (fallback honoured)

ctx.setdefault('length', 999) -> 999         <- bypasses the fallback and shadows the parent key
ctx.pop('length')             -> KeyError    <- while ctx['length'] succeeds
ctx == dict(ctx)              -> False       <- does not equal its own union view

setdefault and pop are dict's C-level implementations, which bypass
__missing__ entirely — the same reason __contains__ and get had to be
overridden in the first place. For __eq__ the asymmetry is visible in one line:
len(ctx) is 3 and ctx.keys() yields ['__packet__', 'length', 'other']
because __len__/__iter__ present the union, while dict.keys(ctx) — the real
local storage — holds only ['__packet__']. So equality compares a different set
of keys from the one iteration reports.

For completeness, **-unpacking is fine: dict(**ctx) returns all three
keys, because overriding __iter__ forces CPython off its dict-merge fast path.
That was checked rather than assumed.

Suggested direction

Either override the three to honour the fallback, or make the class explicitly
refuse them — setdefault and pop raising UnsupportedCall would be
defensible, since a nested context arguably has no business supporting either,
and an explicit refusal is better than a silently divergent answer. Whichever is
chosen, __eq__ and __len__/__iter__ should agree on which key set they
describe.

Worth checking at the same time whether any other dict method presents the same
split: keys, values, items, __len__ and __iter__ are overridden or
inherited consistently today, but update, copy (already overridden — see
pcapkit/protocols/schema/schema.py:751, which needs it) and popitem are worth
a look.

Also, a stale docstring

tests/corekit/test_fields_misc_packet_context.py:24 still describes the
mechanism as collections.ChainMap. That was true of an earlier revision of #457
and stopped being true when it pivoted to a dict subclass to survive Python
3.10. Cosmetic, but misleading to the next reader.

Provenance

Surfaced by the review of #457 as explicitly non-blocking, and verified
independently here — my first measurement of pop was contaminated by a
preceding setdefault on the same context and had to be redone on a fresh one.
Not folded into #457, which was already at a GOOD TO MERGE verdict.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions