diff --git a/CHANGES.md b/CHANGES.md index b14f8a41..b20f1605 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ In development ============== +3.1.2 +===== + +- Fix pickling of classes containing type annotations for Python 3.14 + ([PR#578](https://github.com/cloudpipe/cloudpickle/pull/578)) + 3.1.1 ===== diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 4f4d857a..e600b35f 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -783,6 +783,12 @@ def _class_getstate(obj): clsdict.pop("__dict__", None) # unpicklable property object + if sys.version_info >= (3, 14): + # PEP-649/749: __annotate_func__ contains a closure that references the class + # dict. We need to exclude it from pickling. Python will recreate it when + # __annotations__ is accessed at unpickling time. + clsdict.pop("__annotate_func__", None) + return (clsdict, {}) @@ -1190,6 +1196,10 @@ def _class_setstate(obj, state): for subclass in registry: obj.register(subclass) + # PEP-649/749: During pickling, we excluded the __annotate_func__ attribute but it + # will be created by Python. Subsequently, annotations will be recreated when + # __annotations__ is accessed. + return obj diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 45e8c592..7b9cb749 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -2672,6 +2672,22 @@ class C: C1 = pickle_depickle(C, protocol=self.protocol) assert C1.__annotations__ == C.__annotations__ + def test_class_annotations_abstractclass(self): + # see https://github.com/cloudpipe/cloudpickle/issues/572 + + class C(abc.ABC): + a: int + + C1 = pickle_depickle(C, protocol=self.protocol) + assert C1.__annotations__ == C.__annotations__ + C2 = pickle_depickle(C1, protocol=self.protocol) + if sys.version_info >= (3, 14): + # check that __annotate_func__ is created by Python + assert hasattr(C2, "__annotate_func__") + assert C2.__annotations__ == C1.__annotations__ + c2 = C2() + assert isinstance(c2, C2) + def test_function_annotations(self): def f(a: int) -> str: pass