Repository navigation
fix(reassembly): make Deferred picklable under protocol 0 and 1 - #2424
Conversation
|
Review: GOOD TO GO at Two non-blocking findings worth tracking, neither reachable in the repo today (no stored
Filing a lightweight follow-up for both; not blocking this PR. |
6cf10fa to
43f7e2b
Compare
|
Review: NEEDS CHANGES at
Full review in the task transcript if useful. Re-requesting changes on the current head. |
|
Review: NEEDS CHANGES at
Everything else verified independently and holds: single commit on current |
Deferred declares __slots__ with no __getstate__/__setstate__, so protocol 0 and 1 (which copyreg refuses outright once __getstate__ is still the inherited default) raised TypeError for any Deferred instance, regardless of its analyze/proto/payload values. Independent of #2417, a different concern on the same class. Two cross-review rounds before this merged raised further defects on the same fix (#2425), all folded in here: - __getstate__ mirrors the (dict_or_None, slots) pair Python's own default slotted-class reduction already produced for every released build, instead of a bespoke flat dict. A Deferred pickled by any released version still unpickles, and __setstate__ additionally still accepts the first revision's own flat shape. - slot names come from copyreg._slotnames(type(self)) rather than a hand-rolled walk of type(self).__mro__ and each class's __slots__, which got three cases wrong: a subclass's own __slots__ shadowing the base class's (self.__slots__ resolves through the MRO), a bare-string __slots__ = 'name' read character by character, and a private, name-mangled slot read back under its unmangled spelling. - the pickle test file's own native-shape comparison helper used object.__getstate__, which is 3.11+ only; it now reads the identical state through __reduce_ex__(2)[2], available on every interpreter this project supports, and the file's tier is 'ALL' rather than 'LATEST', matching the house convention for pickling/copying behaviour. Covered by tests/foundation/reassembly/test_deferred_pickle_2423_unit.py: protocol 0/1, the native state shape, backward compatibility with both pickle shapes, MRO-aware subclassing (unset slots, a __weakref__ slot, a bare __dict__), and real pickle round trips for a bare-string and a name-mangled subclass slot. Confirmed every new/changed test fails against each prior revision by stashing just the relevant code change and re-running. Build: brazil-build not applicable (GitHub project); ran coverage run -m pytest, mypy and pylint with this project's own CI flags, and isort -- all clean on the touched files. Closes #2423 Closes #2425
43f7e2b to
03f828e
Compare
|
Review: GOOD TO GO at Diff vs the prior reviewed head (
No scope creep, no regressions found. |
|
Review: GOOD TO GO at Both round-3 fixes confirmed:
Full |
Round 10, the closing-condition test: a full re-grep across the entire repo found exactly 3 items, all outside the carve-outs round 9 already settled. * pcapkit/protocols/protocol.py + foundation/registry/protocols.py: RegistryError said "protocol must be a Protocol subclass" while the adjacent issubclass() check and the Raises: docstring both say ProtocolBase -- fixed the message text to match, with the three test assertions that locked the old string. * tests/foundation/reassembly/test_deferred_pickle_2423_unit.py: a docstring said "this pull request's earlier commit", stale now that #2424 landed as one squashed commit on main -- reworded to cite the issue instead of the PR's own history. * tests/protocols/transport/test_tcp_mptcp_option_attr_2293_unit.py: a cross-reference cited the nonexistent module pcapkit.corekit.fields.enums.EnumField; the real path is pcapkit.corekit.fields.numbers.EnumField. No executable logic changed: an AST structural diff (ignoring string constants) against origin/main is identical for every touched file, and the RegistryError fix only changes the text of a raised message, not what triggers it. Re-verified and rejected two relayed findings that didn't survive a direct check: a transport.py:188-189 and a registry/protocols.py:370 cross-reference claim both already read correctly on this base. Re-swept vendor/process() for siblings of #2429/#2430 -- none found. docs/source and the rest of tests/ came back clean. Build: tests/protocols/test_register_class_guard_unit.py, tests/foundation/registry/test_protocols.py, tests/foundation/reassembly/test_deferred_pickle_2423_unit.py, tests/protocols/transport/test_tcp_mptcp_option_attr_2293_unit.py and tests/project/test_docs_issue_pr_roles_1103_unit.py pass. Closes #1103.
…pickling copyreg._reduce_ex, which object.__reduce_ex__ hands protocol 0/1 to, walks cls.__mro__ for a base to compute state from, before __getstate__ is ever consulted. On CPython that walk passes through Generic and lands on object (state=None); on GraalPy 25.4 it stops at Generic itself and calls Generic(self), raising "TypeError: typing.Generic() takes no arguments" -- independent of #2424/#2425's __getstate__/__setstate__ fix, since those only supply the dict half of a reduction the walk fails to even build. - Deferred.__reduce_ex__ now hard-codes base=object, state=None for protocol 0/1 -- the only outcome the walk ever reached for this class -- and builds the reduction tuple directly via copyreg._reconstructor, with __getstate__()'s own (dict_or_None, slots) pair restored through __setstate__ exactly as before. Protocol >= 2 defers to object.__reduce_ex__ unchanged. - Dropping Generic[_KT] instead was considered and rejected: test_deferred_proto_type_2417_unit.py reads Deferred.__parameters__ at runtime, which only exists because Generic is a real base, not a TYPE_CHECKING-only annotation. No longer invokes copyreg._reduce_ex's MRO walk for protocol<2 at all, so Generic is no longer part of the route pickling takes -- expected to resolve the GraalPy failure the same way on that interpreter, though it cannot be run here to confirm directly. Build/test: mypy 98 errors/32 files (baseline, unchanged), pylint 10.00/10, isort clean; tests/foundation/reassembly (173 tests/882 subtests) and tests/corekit (866 tests/7649 subtests) pass; 100% coverage on the changed file. Closes #2433
…pickling copyreg._reduce_ex, which object.__reduce_ex__ hands protocol 0/1 to, walks cls.__mro__ for a base to compute state from, before __getstate__ is ever consulted. On CPython that walk passes through Generic (a heap type whose __new__ is just the inherited, differently-bound object.__new__) and stops at object (state=None); on GraalPy 25.4 Generic is itself a non-heap (built-in) type, so the walk's first check breaks there directly and calls Generic(self), raising "TypeError: typing.Generic() takes no arguments" -- independent of #2424/#2425's __getstate__/__setstate__ fix, since those only supply the dict half of a reduction the walk fails to even build. - Deferred.__reduce_ex__ now hard-codes base=object, state=None for protocol 0/1 -- the only outcome the walk ever reached for this class -- and builds the reduction tuple directly via copyreg._reconstructor, with __getstate__()'s own (dict_or_None, slots) pair restored through __setstate__ exactly as before. Protocol >= 2 defers to object.__reduce_ex__ unchanged. - It also checks for, and defers to, a subclass's own __reduce__ override before building that hard-coded tuple, matching what object.__reduce_ex__ itself already does for every protocol. Without the check, a Deferred subclass overriding __reduce__ had it silently dropped at protocol 0/1 while protocol 2+ (via object.__reduce_ex__ unchanged) kept honouring it -- a regression caught in cross-review, now covered by a new test alongside the GraalPy-failure regression test. - Dropping Generic[_KT] instead was considered and rejected: test_deferred_proto_type_2417_unit.py reads Deferred.__parameters__ at runtime, which only exists because Generic is a real base, not a TYPE_CHECKING-only annotation. No longer invokes copyreg._reduce_ex's MRO walk for protocol<2 at all, so Generic is no longer part of the route pickling takes. Confirmed on real GraalPy 25.4.4 by cross-review: test_deferred_pickle_2423_unit.py goes from 8 failed on main to 16 passed/18 subtests on this fix, and the full tests/foundation/reassembly/ suite passes 173/854 subtests, 0 failed. Build/test (CPython): mypy 98 errors/32 files (baseline, unchanged), pylint 10.00/10, isort clean; tests/foundation/reassembly (176 tests/890 subtests, including the two new regression tests) and tests/corekit (693 tests/6767 subtests, unaffected by this change) pass; 100% coverage on the changed file. Closes #2433
…pickling copyreg._reduce_ex, which object.__reduce_ex__ hands protocol 0/1 to, walks cls.__mro__ for a base to compute state from, before __getstate__ is ever consulted. On CPython that walk passes through Generic (a heap type whose __new__ is just the inherited, differently-bound object.__new__) and stops at object (state=None); on GraalPy 25.4 Generic is itself a non-heap (built-in) type, so the walk's first check breaks there directly and calls Generic(self), raising "TypeError: typing.Generic() takes no arguments" -- independent of #2424/#2425's __getstate__/__setstate__ fix, since those only supply the dict half of a reduction the walk fails to even build. - Deferred.__reduce_ex__ now hard-codes base=object, state=None for protocol 0/1 -- the only outcome the walk ever reached for this class -- and builds the reduction tuple directly via copyreg._reconstructor, with __getstate__()'s own (dict_or_None, slots) pair restored through __setstate__ exactly as before. Protocol >= 2 defers to object.__reduce_ex__ unchanged. - It also checks for, and defers to, a subclass's own __reduce__ override before building that hard-coded tuple, matching what object.__reduce_ex__ itself already does for every protocol. Without the check, a Deferred subclass overriding __reduce__ had it silently dropped at protocol 0/1 while protocol 2+ (via object.__reduce_ex__ unchanged) kept honouring it -- a regression caught in cross-review, now covered by a new test alongside the GraalPy-failure regression test. - Dropping Generic[_KT] instead was considered and rejected: test_deferred_proto_type_2417_unit.py reads Deferred.__parameters__ at runtime, which only exists because Generic is a real base, not a TYPE_CHECKING-only annotation. No longer invokes copyreg._reduce_ex's MRO walk for protocol<2 at all, so Generic is no longer part of the route pickling takes. Confirmed on real GraalPy 25.4.4 by cross-review: test_deferred_pickle_2423_unit.py goes from 8 failed on main to 16 passed/18 subtests on this fix, and the full tests/foundation/reassembly/ suite passes 173/854 subtests, 0 failed. Build/test (CPython): mypy 98 errors/32 files (baseline, unchanged), pylint 10.00/10, isort clean; tests/foundation/reassembly (176 tests/890 subtests, including the two new regression tests) and tests/corekit (693 tests/6767 subtests, unaffected by this change) pass; 100% coverage on the changed file. Closes #2433
…pickling (#2435) copyreg._reduce_ex, which object.__reduce_ex__ hands protocol 0/1 to, walks cls.__mro__ for a base to compute state from, before __getstate__ is ever consulted. On CPython that walk passes through Generic (a heap type whose __new__ is just the inherited, differently-bound object.__new__) and stops at object (state=None); on GraalPy 25.4 Generic is itself a non-heap (built-in) type, so the walk's first check breaks there directly and calls Generic(self), raising "TypeError: typing.Generic() takes no arguments" -- independent of #2424/#2425's __getstate__/__setstate__ fix, since those only supply the dict half of a reduction the walk fails to even build. - Deferred.__reduce_ex__ now hard-codes base=object, state=None for protocol 0/1 -- the only outcome the walk ever reached for this class -- and builds the reduction tuple directly via copyreg._reconstructor, with __getstate__()'s own (dict_or_None, slots) pair restored through __setstate__ exactly as before. Protocol >= 2 defers to object.__reduce_ex__ unchanged. - It also checks for, and defers to, a subclass's own __reduce__ override before building that hard-coded tuple, matching what object.__reduce_ex__ itself already does for every protocol. Without the check, a Deferred subclass overriding __reduce__ had it silently dropped at protocol 0/1 while protocol 2+ (via object.__reduce_ex__ unchanged) kept honouring it -- a regression caught in cross-review, now covered by a new test alongside the GraalPy-failure regression test. - Dropping Generic[_KT] instead was considered and rejected: test_deferred_proto_type_2417_unit.py reads Deferred.__parameters__ at runtime, which only exists because Generic is a real base, not a TYPE_CHECKING-only annotation. No longer invokes copyreg._reduce_ex's MRO walk for protocol<2 at all, so Generic is no longer part of the route pickling takes. Confirmed on real GraalPy 25.4.4 by cross-review: test_deferred_pickle_2423_unit.py goes from 8 failed on main to 16 passed/18 subtests on this fix, and the full tests/foundation/reassembly/ suite passes 173/854 subtests, 0 failed. Build/test (CPython): mypy 98 errors/32 files (baseline, unchanged), pylint 10.00/10, isort clean; tests/foundation/reassembly (176 tests/890 subtests, including the two new regression tests) and tests/corekit (693 tests/6767 subtests, unaffected by this change) pass; 100% coverage on the changed file. Closes #2433
Please follow the guide below
mypy/pylint/isortclean on the touched file, using this project's own CI flags)docs/source/changelog/and regeneratedCHANGELOG.mdWhat is the purpose of your pull request?
fix— corrects a defectDescription of your pull request and other information
Deferred(pcapkit/foundation/reassembly/data/data.py) declares__slots__with no
__getstate__/__setstate__, sopickle.dumps(d, protocol=0)andprotocol=1raisedTypeErrorfor any instance. Protocols 2+ alreadyworked. Independent of #2417 (also on
Deferred, different concern) and#2421 (different, unrelated class).
Two cross-review rounds raised further defects on the same fix, now tracked
as #2425 and folded into this same PR:
__getstate__returned a flat{slot: value}dict —a different shape from the
(dict_or_None, slots)pair every releasedbuild already used. Now mirrors the native shape;
__setstate__stillaccepts the flat shape too.
self.__slots__resolves through the MRO like any other attribute, and ahand-rolled MRO walk also mishandled a bare-string
__slots__ = 'name'and a private, name-mangled slot. Replaced with
copyreg._slotnames, thestandard library's own helper for exactly this.
object.__getstate__, 3.11+only; now reads the same state via
__reduce_ex__(2)[2], and the file'stier is
ALLrather thanLATEST, matching house convention forpickling/copying behaviour.
tests/foundation/reassembly/test_deferred_pickle_2423_unit.pycoversprotocol 0/1, the native state shape, backward compatibility with both
pickle shapes, MRO-aware subclassing (unset slots,
__weakref__, a bare__dict__), and real pickle round trips for bare-string and name-mangledsubclass slots. Confirmed every new/changed test fails against each prior
revision by stashing just that code change and re-running.
Closes #2423
Closes #2425