Repository navigation
fix: NEWOBJ_EX objects re-encode as copyreg.__newobj_ex__ REDUCE (#35) - #57
Merged
Merged
Conversation
`cls args kwargs NEWOBJ_EX` becomes `Reduce { copyreg.__newobj_ex__,
(cls, args, kwargs) }`, the callable CPython's own `object.__reduce_ex__`
returns for `__getnewargs_ex__` objects (the pickler rewrites it into
`NEWOBJ_EX` only at protocol 4). It is a plain REDUCE for the writers,
readers and encoders, loads to `cls.__new__(cls, *args, **kwargs)` on any
Python 3.4+, and a following BUILD becomes its state. The `@args`/`@kwargs`
dict the decoder used to build produced a pickle CPython rejects.
Assisted-by: Claude Fable 5.1
The encoders accept the shape earlier releases wrote for NEWOBJ_EX objects (`newobj` with a dict `args` holding `@args` and `@kwargs`, also nested in the `@args`/`@state` state of an instance) and emit the same `copyreg.__newobj_ex__(cls, args, kwargs) REDUCE` call the decoder now produces. Python tests round-trip `__getnewargs_ex__` classes through every path at protocol 4, with and without state, and load both legacy shapes. Assisted-by: Claude Fable 5.1
…ies (#35) Fresh review findings: 1.6.1 wrote the NEWOBJ_EX shape without a `newobj` key (the field did not exist), so the translation to the copyreg call must not depend on it; a dict is never a valid REDUCE or NEWOBJ argument, so the two-key `@args`/`@kwargs` dict is translated wherever it appears. The tests build ZODB records with one pickler (shared memo, with a memo reference in the state), add true empty args and kwargs through a custom `__reduce_ex__`, and the 1.6.1 shape; the args and kwargs of the call are encoded one level deeper, as items of the tuple; the docs say the encoders translate the old shape rather than that readers accept it. Assisted-by: Claude Fable 5.1
jensens
force-pushed
the
fix/newobj-ex
branch
2 times, most recently
from
September 30, 2026 15:17
c86b976 to
5afc700
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #35
Built on #56 (
fix/reduce-build-newobj) and targetingmainso CI runs: the diff shows #56's four commits plus four of its own. Merge #56 first and this shrinks to its own commits; merging this one first brings #56 along.The problem
A protocol 4 pickle creating an object with
NEWOBJ_EX(__getnewargs_ex__with keyword arguments) decoded into aReducewhose args were a dict{"@args": ..., "@kwargs": ...}, and re-encoding wroteGLOBAL <dict> NEWOBJ, which CPython rejects (NEWOBJ args argument must be a tuple); with a followingBUILDthe object came back without its constructor arguments. ZODB writes protocol 3, so onlypickle_to_jsonof protocol 4 pickles met it.Design (a fork the issue left open, decided in the plan)
Instead of a new
kwargskey, the decoder turnscls args kwargs NEWOBJ_EXintocopyreg.__newobj_ex__(cls, args, kwargs)withnewobj: false: the callable CPython's ownobject.__reduce_ex__returns for__getnewargs_ex__objects (the pickler only rewrites it intoNEWOBJ_EXat protocol 4). It is a plainREDUCEfor every writer, reader and encoder, loads tocls.__new__(cls, *args, **kwargs)on any Python 3.4 or newer, and a followingBUILDbecomes its state. The reviewer checked the ruling against both CPython unpicklers: the REDUCE form is never less permissive thanNEWOBJ_EXand exactly as permissive as the pure-Python unpickler.The encoders also translate the shape 1.6.x wrote for these objects (the two-key
@args/@kwargsdict asargs, with or without anewobjkey, and nested in an instance's@args/@statestate) into the same call, so oldpickle_to_jsonoutput becomes loadable. A dict is never a validREDUCE/NEWOBJargument, so nothing loadable is misread.Evidence
__getnewargs_ex__classes through every path at protocol 4, with and without state, with true empty args and kwargs (a custom__reduce_ex__, the only way CPython emits them), through ZODB records written by one pickler (shared memo), and load both legacy shapes.__getnewargs_ex__shapes at protocols 2 to 5 through five paths againstpickle.loadsof the source with the C and the pure-Python unpickler, nestedNEWOBJ_EX1,100 deep (clean error, no crash), and the 1.6.1 wheel's actual output. Its one Important finding (the 1.6.1 shape has nonewobjkey, the translation was gated on it) is the last commit.Assisted-by: Claude Fable 5.1