1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
From: =?utf-8?q?Lum=C3=ADr_=27Frenzy=27_Balhar?= <lbalhar@redhat.com>
Date: Fri, 11 Oct 2024 17:47:37 +0200
Subject: Accommodate class state restoration for Python 3.13 (#534)
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org>
Co-authored-by: Pierre Glaser <pierreglaser@msn.com>
Origin: backport, https://github.com/cloudpipe/cloudpickle/commit/b3bac2c3aaecf682a6a0099b26370b2ff12c530b
Last-Update: 2025-01-25
---
srsly/cloudpickle/cloudpickle.py | 11 +++++++++++
srsly/tests/cloudpickle/cloudpickle_test.py | 25 ++++++++++++++++++++++++-
2 files changed, 35 insertions(+), 1 deletion(-)
diff --git a/srsly/cloudpickle/cloudpickle.py b/srsly/cloudpickle/cloudpickle.py
index 317be69..2697090 100644
--- a/srsly/cloudpickle/cloudpickle.py
+++ b/srsly/cloudpickle/cloudpickle.py
@@ -842,6 +842,17 @@ def _rehydrate_skeleton_class(skeleton_class, class_dict):
registry = attr
else:
setattr(skeleton_class, attrname, attr)
+
+ if sys.version_info >= (3, 13) and "__firstlineno__" in class_dict:
+ # Set the Python 3.13+ only __firstlineno__ attribute one more time, as it
+ # will be automatically deleted by the `setattr(obj, attrname, attr)` call
+ # above when `attrname` is "__firstlineno__". We assume that preserving this
+ # information might be important for some users and that it not stale in the
+ # context of cloudpickle usage, hence legitimate to propagate. Furthermore it
+ # is necessary to do so to keep deterministic chained pickling as tested in
+ # test_deterministic_str_interning_for_chained_dynamic_class_pickling.
+ skeleton_class.__firstlineno__ = class_dict["__firstlineno__"]
+
if registry is not None:
for subclass in registry:
skeleton_class.register(subclass)
diff --git a/srsly/tests/cloudpickle/cloudpickle_test.py b/srsly/tests/cloudpickle/cloudpickle_test.py
index e4dba00..3aead77 100644
--- a/srsly/tests/cloudpickle/cloudpickle_test.py
+++ b/srsly/tests/cloudpickle/cloudpickle_test.py
@@ -112,7 +112,12 @@ def test_extract_class_dict():
return "c"
clsdict = _extract_class_dict(C)
- assert sorted(clsdict.keys()) == ["C_CONSTANT", "__doc__", "method_c"]
+ expected_keys = ["C_CONSTANT", "__doc__", "method_c"]
+ # New attribute in Python 3.13 beta 1
+ # https://github.com/python/cpython/pull/118475
+ if sys.version_info >= (3, 13):
+ expected_keys.insert(2, "__firstlineno__")
+ assert sorted(clsdict.keys()) == expected_keys
assert clsdict["C_CONSTANT"] == 43
assert clsdict["__doc__"] is None
assert clsdict["method_c"](C()) == C().method_c()
@@ -341,6 +346,24 @@ class CloudPickleTest(unittest.TestCase):
g = pickle_depickle(f(), protocol=self.protocol)
self.assertEqual(g(), 2)
+ def test_class_no_firstlineno_deletion_(self):
+ # `__firstlineno__` is a new attribute of classes introduced in Python 3.13.
+ # This attribute used to be automatically deleted when unpickling a class as a
+ # consequence of cloudpickle setting a class's `__module__` attribute at
+ # unpickling time (see https://github.com/python/cpython/blob/73c152b346a18ed8308e469bdd232698e6cd3a63/Objects/typeobject.c#L1353-L1356).
+ # This deletion would cause tests like
+ # `test_deterministic_dynamic_class_attr_ordering_for_chained_pickling` to fail.
+ # This test makes sure that the attribute `__firstlineno__` is preserved
+ # across a cloudpickle roundtrip.
+
+ class A:
+ pass
+
+ if hasattr(A, "__firstlineno__"):
+ A_roundtrip = pickle_depickle(A, protocol=self.protocol)
+ assert hasattr(A_roundtrip, "__firstlineno__")
+ assert A_roundtrip.__firstlineno__ == A.__firstlineno__
+
def test_dynamically_generated_class_that_uses_super(self):
class Base:
|