Conversation
A PEP 649 __annotate__ function closes over the namespace in which the annotated object was defined. For a method, that is the namespace of the class, which for an ABC holds an unpicklable _abc_impl object. When such an annotate function has to be pickled by value -- for instance after functools.update_wrapper copied it onto a wrapper instance on Python 3.14+ -- cloudpickle tries to serialize that namespace and fails with "TypeError: cannot pickle '_abc._abc_data' object". Evaluate the annotations at pickling time instead and pickle a plain annotate function returning them, mirroring what _function_getstate already does for the __annotations__ of dynamic functions. When the annotations cannot be evaluated eagerly, fall back to the generic dynamic function reducer so that lazy annotations keep working. Fixes cloudpipe#585.
This branch has not been deployed
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 #585. Opened as a separate PR from #594 so that the two approaches can be compared side by side; only one of them should be merged.
What actually goes wrong
A PEP 649
__annotate__function closes over the namespace in which the annotated object was defined. For a method, that is the namespace of the class, which for an ABC contains_abc_impl(an unpicklable_abc._abc_dataobject):Reading that expression inside-out:
Impl.mis the plain function object of the method..__annotate__is the function the compiler emits per annotated object since PEP 649: annotations are no longer evaluated at definition time, they are computed on demand by this function.Impl.m.__annotate__(Format.VALUE)returns
{'x': int, 'return': str}, which is what a first access toImpl.m.__annotations__triggers..__closure__exists because that function runs later and still has to resolve the names used in the annotations as they were visible where the method was defined. For a method that scope is the class body, so the annotatefunction is a closure over the class namespace.
[0].cell_contentsunwraps the single captured cell, which is the dict used as that class namespace.sorted(...)lists its keys.The interesting key is
_abc_impl: writingdef m(self, x: int) -> strin an ABC is enough for the method's annotate function to transitively hold a reference to the class's_abc._abc_dataobject, which pickle cannot serialize.As long as that function is pickled by reference nothing happens. It becomes a problem whenever cloudpickle has to pickle it by value, and
functools.update_wrapperis the common way for a bare__annotate__functionto end up somewhere cloudpickle has to look at it: since Python 3.14
__annotate__is part ofWRAPPER_ASSIGNMENTS, so it is copied into the wrapper instance's__dict__.There are two situations where the by-value path is taken:
__main__, in a notebook, or otherwise dynamic).__qualname__of a method's__annotate__function wrongly pointed at the class (Impl.__annotate__instead ofImpl.m.__annotate__), so the by-reference lookup failed for regular importable classes too.Situation 2 is fixed upstream, in the 3.14 branch by gh-148221 and on main by gh-137842. On a patched CPython (checked with 3.14.6 and 3.15.0b3) the module-level reproducer of #585 already works with cloudpickle
master. Situation 1 still fails on every 3.14+ build, and the fix below covers both.The change
Follow what cloudpickle already does for the annotations of dynamic functions:
_function_getstatestores__annotations__, i.e. it evaluates them at pickling time rather than shipping the lazy annotate closure. Do the same for a bare__annotate__function that has to be pickled by value: evaluate it withannotationlib.call_annotate_function(func, Format.VALUE)and pickle a small annotate function returning that snapshot, supporting theVALUE,FORWARDREFandSTRINGformats.If the annotations cannot be evaluated eagerly (unresolvable forward references, for example), the reducer returns
NotImplementedand the generic dynamic function reducer is used, exactly as today — so nothing that works right now changes behaviour.cloudpickle/cloudpickle.py:_make_eager_annotate,_eager_annotate_reduceand a single hook inPickler._function_reduce.tests/cloudpickle_test.py: regression test for theupdate_wrapper+ annotated ABC method case ofTypeError: cannot pickle '_abc._abc_data' objectwith python 3.14 and cloudpickle 3.1.2 #585, plus tests for pickling a bare__annotate__function and for annotations that must stay lazy.CHANGES.md: changelog entry.How this differs from #594
#594 detects an
__annotate__entry copied byupdate_wrapper(an object with__wrapped__and a function called__annotate__in its state) and drops it from the pickled state.master— comes back without anyannotations at all (
annotationlib.get_annotations(clone)raisesTypeError: ... does not have annotations). Before 3.14,update_wrappercopied__annotations__onto the wrapper and it survived pickling, so this isa behaviour regression.
__wrapped__is incidental; the problem is the by-value pickling of an annotate closure. A bareImpl.m.__annotate__still fails with Fix Python 3.14 update_wrapper pickling with lazy annotations #594.__wrapped__attribute and a function named__annotate__loses that attribute silently, including when it was set deliberately.This PR keeps the annotations, does not need the
__wrapped__heuristic, and adds no per-object cost inreducer_override.Validation
master__main__Impl.m.__annotate__of a dynamic classVALUEandSTRING)Test suite, all green: 256 passed on CPython 3.14.4 (pre-gh-137814-fix build),
257 on 3.14.6, 256 on 3.15.0b3 and 252 on 3.12.
Reproducer used for the table: