Skip to content

Commit b3bac2c

Browse files
frenzymadnessogriselpierreglaser
authored
Accomodate 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>
1 parent bc677da commit b3bac2c

File tree

5 files changed

+51
-17
lines changed

5 files changed

+51
-17
lines changed

.github/workflows/testing.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@v4
1414
- name: Set up Python 3.11
15-
uses: actions/setup-python@v4
15+
uses: actions/setup-python@v5
1616
with:
1717
python-version: 3.11
1818
- name: Install pre-commit
@@ -29,7 +29,7 @@ jobs:
2929
strategy:
3030
matrix:
3131
os: [ubuntu-latest, windows-latest, macos-latest]
32-
python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9"]
32+
python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9"]
3333
exclude:
3434
# Do not test all minor versions on all platforms, especially if they
3535
# are not the oldest/newest supported versions
@@ -50,7 +50,7 @@ jobs:
5050
steps:
5151
- uses: actions/checkout@v4
5252
- name: Set up Python ${{ matrix.python_version }}
53-
uses: actions/setup-python@v4
53+
uses: actions/setup-python@v5
5454
with:
5555
python-version: ${{ matrix.python_version }}
5656
allow-prereleases: true
@@ -91,7 +91,7 @@ jobs:
9191
steps:
9292
- uses: actions/checkout@v4
9393
- name: Set up Python
94-
uses: actions/setup-python@v4
94+
uses: actions/setup-python@v5
9595
with:
9696
python-version: ${{ matrix.python_version }}
9797
- name: Install project and dependencies
@@ -127,7 +127,7 @@ jobs:
127127
steps:
128128
- uses: actions/checkout@v4
129129
- name: Set up Python
130-
uses: actions/setup-python@v4
130+
uses: actions/setup-python@v5
131131
with:
132132
python-version: ${{ matrix.python_version }}
133133
- name: Install project and dependencies
@@ -155,7 +155,7 @@ jobs:
155155
steps:
156156
- uses: actions/checkout@v4
157157
- name: Set up Python
158-
uses: actions/setup-python@v4
158+
uses: actions/setup-python@v5
159159
with:
160160
python-version: ${{ matrix.python_version }}
161161
- name: Install downstream project and dependencies
@@ -180,7 +180,7 @@ jobs:
180180
steps:
181181
- uses: actions/checkout@v4
182182
- name: Set up Python
183-
uses: actions/setup-python@v4
183+
uses: actions/setup-python@v5
184184
with:
185185
python-version: ${{ matrix.python_version }}
186186
- name: Install project and tests dependencies

CHANGES.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
======================
33

44
- Some improvements to make cloudpickle more deterministic when pickling
5-
dynamic functions and classes.
6-
([PR #524](https://github.com/cloudpipe/cloudpickle/pull/524))
5+
dynamic functions and classes, in particular with CPython 3.13.
6+
([PR #524](https://github.com/cloudpipe/cloudpickle/pull/524) and
7+
[PR #534](https://github.com/cloudpipe/cloudpickle/pull/534))
78

89
- Fix a problem with the joint usage of cloudpickle's `_whichmodule` and
910
`multiprocessing`.

cloudpickle/cloudpickle.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def func():
370370
# sys.modules.
371371
if name is not None and name.startswith(prefix):
372372
# check whether the function can address the sub-module
373-
tokens = set(name[len(prefix):].split("."))
373+
tokens = set(name[len(prefix) :].split("."))
374374
if not tokens - set(code.co_names):
375375
subimports.append(sys.modules[name])
376376
return subimports
@@ -707,7 +707,7 @@ def _function_getstate(func):
707707
# Hack to circumvent non-predictable memoization caused by string interning.
708708
# See the inline comment in _class_setstate for details.
709709
"__name__": "".join(func.__name__),
710-
"__qualname__": func.__qualname__,
710+
"__qualname__": "".join(func.__qualname__),
711711
"__annotations__": func.__annotations__,
712712
"__kwdefaults__": func.__kwdefaults__,
713713
"__defaults__": func.__defaults__,
@@ -1167,6 +1167,17 @@ def _class_setstate(obj, state):
11671167
# Indeed the Pickler's memoizer relies on physical object identity to break
11681168
# cycles in the reference graph of the object being serialized.
11691169
setattr(obj, attrname, attr)
1170+
1171+
if sys.version_info >= (3, 13) and "__firstlineno__" in state:
1172+
# Set the Python 3.13+ only __firstlineno__ attribute one more time, as it
1173+
# will be automatically deleted by the `setattr(obj, attrname, attr)` call
1174+
# above when `attrname` is "__firstlineno__". We assume that preserving this
1175+
# information might be important for some users and that it not stale in the
1176+
# context of cloudpickle usage, hence legitimate to propagate. Furthermore it
1177+
# is necessary to do so to keep deterministic chained pickling as tested in
1178+
# test_deterministic_str_interning_for_chained_dynamic_class_pickling.
1179+
obj.__firstlineno__ = state["__firstlineno__"]
1180+
11701181
if registry is not None:
11711182
for subclass in registry:
11721183
obj.register(subclass)

tests/cloudpickle_test.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ def method_c(self):
110110
return "c"
111111

112112
clsdict = _extract_class_dict(C)
113-
assert list(clsdict.keys()) == ["C_CONSTANT", "__doc__", "method_c"]
113+
expected_keys = ["C_CONSTANT", "__doc__", "method_c"]
114+
# New attribute in Python 3.13 beta 1
115+
# https://github.com/python/cpython/pull/118475
116+
if sys.version_info >= (3, 13):
117+
expected_keys.insert(2, "__firstlineno__")
118+
assert list(clsdict.keys()) == expected_keys
114119
assert clsdict["C_CONSTANT"] == 43
115120
assert clsdict["__doc__"] is None
116121
assert clsdict["method_c"](C()) == C().method_c()
@@ -331,6 +336,25 @@ def g():
331336
g = pickle_depickle(f(), protocol=self.protocol)
332337
self.assertEqual(g(), 2)
333338

339+
def test_class_no_firstlineno_deletion_(self):
340+
# `__firstlineno__` is a new attribute of classes introduced in Python 3.13.
341+
# This attribute used to be automatically deleted when unpickling a class as a
342+
# consequence of cloudpickle setting a class's `__module__` attribute at
343+
# unpickling time (see https://github.com/python/cpython/blob/73c152b346a18ed8308e469bdd232698e6cd3a63/Objects/typeobject.c#L1353-L1356).
344+
# This deletion would cause tests like
345+
# `test_deterministic_dynamic_class_attr_ordering_for_chained_pickling` to fail.
346+
# This test makes sure that the attribute `__firstlineno__` is preserved
347+
# across a cloudpickle roundtrip.
348+
349+
class A:
350+
pass
351+
352+
if hasattr(A, "__firstlineno__"):
353+
A_roundtrip = pickle_depickle(A, protocol=self.protocol)
354+
assert hasattr(A_roundtrip, "__firstlineno__")
355+
assert A_roundtrip.__firstlineno__ == A.__firstlineno__
356+
357+
334358
def test_dynamically_generated_class_that_uses_super(self):
335359
class Base:
336360
def method(self):
@@ -2067,7 +2091,7 @@ class A:
20672091
# If the `__doc__` attribute is defined after some other class
20682092
# attribute, this can cause class attribute ordering changes due to
20692093
# the way we reconstruct the class definition in
2070-
# `_make_class_skeleton`, which creates the class and thus its
2094+
# `_make_skeleton_class`, which creates the class and thus its
20712095
# `__doc__` attribute before populating the class attributes.
20722096
class A:
20732097
name = "A"
@@ -2078,7 +2102,7 @@ class A:
20782102

20792103
# If a `__doc__` is defined on the `__init__` method, this can
20802104
# cause ordering changes due to the way we reconstruct the class
2081-
# with `_make_class_skeleton`.
2105+
# with `_make_skeleton_class`.
20822106
class A:
20832107
def __init__(self):
20842108
"""Class definition with explicit __init__"""
@@ -2136,8 +2160,6 @@ def test_dynamic_class_determinist_subworker_tuple_memoization(self):
21362160
# Arguments' tuple is memoized in the main process but not in the
21372161
# subprocess as the tuples do not share the same id in the loaded
21382162
# class.
2139-
2140-
# XXX - this does not seem to work, and I am not sure there is an easy fix.
21412163
class A:
21422164
"""Class with potential tuple memoization issues."""
21432165

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py{38, 39, 310, 311, 312, py3}
2+
envlist = py{38, 39, 310, 311, 312, 313, py3}
33

44
[testenv]
55
deps = -rdev-requirements.txt

0 commit comments

Comments
 (0)