Skip to content

Commit c295f5d

Browse files
authored
Merge branch 'master' into master
2 parents a1eb14f + 685bf0c commit c295f5d

File tree

7 files changed

+108
-21
lines changed

7 files changed

+108
-21
lines changed

.github/workflows/testing.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
strategy:
3030
matrix:
3131
os: [ubuntu-latest, windows-latest, macos-latest]
32-
python_version: [3.5, 3.6, 3.7, 3.8, 3.9, "pypy3"]
32+
python_version: [3.6, 3.7, 3.8, 3.9, "3.10-dev", "pypy3"]
3333
exclude:
3434
# Do not test all minor versions on all platforms, especially if they
3535
# are not the oldest/newest supported versions
@@ -58,7 +58,7 @@ jobs:
5858
steps:
5959
- uses: actions/checkout@v1
6060
- name: Set up Python ${{ matrix.python_version }}
61-
uses: actions/setup-python@v1
61+
uses: actions/setup-python@v2
6262
with:
6363
python-version: ${{ matrix.python_version }}
6464
- name: Install project and dependencies
@@ -96,14 +96,16 @@ jobs:
9696

9797
python-nightly:
9898
runs-on: ubuntu-18.04
99+
# This entry is made optional for now, see https://github.com/cloudpipe/cloudpickle/pull/420
100+
if: "contains(github.event.pull_request.labels.*.name, 'ci python-nightly')"
99101
steps:
100102
- uses: actions/checkout@v1
101103
- name: Install Python from ppa:deadsnakes/nightly
102104
run: |
103105
sudo add-apt-repository ppa:deadsnakes/nightly
104106
sudo apt update
105-
sudo apt install python3.10 python3.10-venv python3.10-dev
106-
python3.10 -m venv nightly-venv
107+
sudo apt install python3.11 python3.11-venv python3.11-dev
108+
python3.11 -m venv nightly-venv
107109
echo "$PWD/nightly-venv/bin" >> $GITHUB_PATH
108110
- name: Display Python version
109111
run: python -c "import sys; print(sys.version)"

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
dev
55
===
66

7+
- Python 3.5 is no longer supported.
8+
9+
- Fix a side effect altering dynamic modules at pickling time.
10+
([PR #426](https://github.com/cloudpipe/cloudpickle/pull/426))
11+
712
- Support for pickling type annotations on Python 3.10 as per [PEP 563](
813
https://www.python.org/dev/peps/pep-0563/)
914
([PR #400](https://github.com/cloudpipe/cloudpickle/pull/400))
@@ -16,6 +21,10 @@ dev
1621
ItemsView, following similar strategy for vanilla Python dictionaries.
1722
([PR #423](https://github.com/cloudpipe/cloudpickle/pull/423))
1823

24+
- Suppressed a source of non-determinism when pickling dynamically defined
25+
functions and handles the deprecation of co_lnotab in Python 3.10+.
26+
([PR #428](https://github.com/cloudpipe/cloudpickle/pull/428))
27+
1928
1.6.0
2029
=====
2130

cloudpickle/cloudpickle.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,10 @@ def _extract_code_globals(co):
237237
out_names = _extract_code_globals_cache.get(co)
238238
if out_names is None:
239239
names = co.co_names
240-
out_names = {names[oparg] for _, oparg in _walk_global_ops(co)}
240+
# We use a dict with None values instead of a set to get a
241+
# deterministic order (assuming Python 3.6+) and avoid introducing
242+
# non-deterministic pickle bytes as a results.
243+
out_names = {names[oparg]: None for _, oparg in _walk_global_ops(co)}
241244

242245
# Declaring a function inside another one using the "def ..."
243246
# syntax generates a constant code object corresponding to the one
@@ -248,7 +251,7 @@ def _extract_code_globals(co):
248251
if co.co_consts:
249252
for const in co.co_consts:
250253
if isinstance(const, types.CodeType):
251-
out_names |= _extract_code_globals(const)
254+
out_names.update(_extract_code_globals(const))
252255

253256
_extract_code_globals_cache[co] = out_names
254257

cloudpickle/cloudpickle_fast.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,19 @@ def _enum_getstate(obj):
244244

245245
def _code_reduce(obj):
246246
"""codeobject reducer"""
247-
if hasattr(obj, "co_posonlyargcount"): # pragma: no branch
247+
if hasattr(obj, "co_linetable"): # pragma: no branch
248+
# Python 3.10 and later: obj.co_lnotab is deprecated and constructor
249+
# expects obj.co_linetable instead.
250+
args = (
251+
obj.co_argcount, obj.co_posonlyargcount,
252+
obj.co_kwonlyargcount, obj.co_nlocals, obj.co_stacksize,
253+
obj.co_flags, obj.co_code, obj.co_consts, obj.co_names,
254+
obj.co_varnames, obj.co_filename, obj.co_name,
255+
obj.co_firstlineno, obj.co_linetable, obj.co_freevars,
256+
obj.co_cellvars
257+
)
258+
elif hasattr(obj, "co_posonlyargcount"):
259+
# Backward compat for 3.9 and older
248260
args = (
249261
obj.co_argcount, obj.co_posonlyargcount,
250262
obj.co_kwonlyargcount, obj.co_nlocals, obj.co_stacksize,
@@ -254,6 +266,7 @@ def _code_reduce(obj):
254266
obj.co_cellvars
255267
)
256268
else:
269+
# Backward compat for even older versions of Python
257270
args = (
258271
obj.co_argcount, obj.co_kwonlyargcount, obj.co_nlocals,
259272
obj.co_stacksize, obj.co_flags, obj.co_code, obj.co_consts,
@@ -342,8 +355,13 @@ def _module_reduce(obj):
342355
if _is_importable(obj):
343356
return subimport, (obj.__name__,)
344357
else:
345-
obj.__dict__.pop('__builtins__', None)
346-
return dynamic_subimport, (obj.__name__, vars(obj))
358+
# Some external libraries can populate the "__builtins__" entry of a
359+
# module's `__dict__` with unpicklable objects (see #316). For that
360+
# reason, we do not attempt to pickle the "__builtins__" entry, and
361+
# restore a default value for it at unpickling time.
362+
state = obj.__dict__.copy()
363+
state.pop('__builtins__', None)
364+
return dynamic_subimport, (obj.__name__, state)
347365

348366

349367
def _method_reduce(obj):

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def find_version():
3939
'Operating System :: POSIX',
4040
'Operating System :: Microsoft :: Windows',
4141
'Operating System :: MacOS :: MacOS X',
42-
'Programming Language :: Python :: 3.5',
4342
'Programming Language :: Python :: 3.6',
4443
'Programming Language :: Python :: 3.7',
4544
'Programming Language :: Python :: 3.8',
@@ -51,5 +50,5 @@ def find_version():
5150
'Topic :: System :: Distributed Computing',
5251
],
5352
test_suite='tests',
54-
python_requires='>=3.5',
53+
python_requires='>=3.6',
5554
)

tests/cloudpickle_test.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import logging
1111
import math
1212
from operator import itemgetter, attrgetter
13+
import pickletools
1314
import platform
1415
import random
1516
import shutil
@@ -50,13 +51,15 @@
5051
from cloudpickle.cloudpickle import _lookup_module_and_qualname
5152

5253
from .testutils import subprocess_pickle_echo
54+
from .testutils import subprocess_pickle_string
5355
from .testutils import assert_run_python_script
5456
from .testutils import subprocess_worker
5557

5658
from _cloudpickle_testpkg import relative_imports_factory
5759

5860

5961
_TEST_GLOBAL_VARIABLE = "default_value"
62+
_TEST_GLOBAL_VARIABLE2 = "another_value"
6063

6164

6265
class RaiserOnPickle(object):
@@ -622,6 +625,12 @@ def __reduce__(self):
622625
assert hasattr(depickled_mod.__builtins__, "abs")
623626
assert depickled_mod.f(-1) == 1
624627

628+
# Additional check testing that the issue #425 is fixed: without the
629+
# fix for #425, `mod.f` would not have access to `__builtins__`, and
630+
# thus calling `mod.f(-1)` (which relies on the `abs` builtin) would
631+
# fail.
632+
assert mod.f(-1) == 1
633+
625634
def test_load_dynamic_module_in_grandchild_process(self):
626635
# Make sure that when loaded, a dynamic module preserves its dynamic
627636
# property. Otherwise, this will lead to an ImportError if pickled in
@@ -2107,8 +2116,8 @@ def inner_function():
21072116
return _TEST_GLOBAL_VARIABLE
21082117
return inner_function
21092118

2110-
globals_ = cloudpickle.cloudpickle._extract_code_globals(
2111-
function_factory.__code__)
2119+
globals_ = set(cloudpickle.cloudpickle._extract_code_globals(
2120+
function_factory.__code__).keys())
21122121
assert globals_ == {'_TEST_GLOBAL_VARIABLE'}
21132122

21142123
depickled_factory = pickle_depickle(function_factory,
@@ -2225,10 +2234,13 @@ def method(self, arg: type_) -> type_:
22252234

22262235
def check_annotations(obj, expected_type, expected_type_str):
22272236
assert obj.__annotations__["attribute"] == expected_type
2228-
if sys.version_info >= (3, 10):
2229-
# In Python 3.10, type annotations are stored as strings.
2237+
if sys.version_info >= (3, 11):
2238+
# In Python 3.11, type annotations are stored as strings.
22302239
# See PEP 563 for more details:
22312240
# https://www.python.org/dev/peps/pep-0563/
2241+
# Originaly scheduled for 3.10, then postponed.
2242+
# See this for more details:
2243+
# https://mail.python.org/archives/list/[email protected]/thread/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/
22322244
assert (
22332245
obj.method.__annotations__["arg"]
22342246
== expected_type_str
@@ -2339,6 +2351,32 @@ def __type__(self):
23392351
o = MyClass()
23402352
pickle_depickle(o, protocol=self.protocol)
23412353

2354+
@pytest.mark.skipif(
2355+
sys.version_info < (3, 7),
2356+
reason="Determinism can only be guaranteed for Python 3.7+"
2357+
)
2358+
def test_deterministic_pickle_bytes_for_function(self):
2359+
# Ensure that functions with references to several global names are
2360+
# pickled to fixed bytes that do not depend on the PYTHONHASHSEED of
2361+
# the Python process.
2362+
vals = set()
2363+
2364+
def func_with_globals():
2365+
return _TEST_GLOBAL_VARIABLE + _TEST_GLOBAL_VARIABLE2
2366+
2367+
for i in range(5):
2368+
vals.add(
2369+
subprocess_pickle_string(func_with_globals,
2370+
protocol=self.protocol,
2371+
add_env={"PYTHONHASHSEED": str(i)}))
2372+
if len(vals) > 1:
2373+
# Print additional debug info on stdout with dis:
2374+
for val in vals:
2375+
pickletools.dis(val)
2376+
pytest.fail(
2377+
"Expected a single deterministic payload, got %d/5" % len(vals)
2378+
)
2379+
23422380

23432381
class Protocol2CloudPickleTest(CloudPickleTest):
23442382

tests/testutils.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import os
33
import os.path as op
44
import tempfile
5-
import base64
65
from subprocess import Popen, check_output, PIPE, STDOUT, CalledProcessError
76
from cloudpickle.compat import pickle
87
from contextlib import contextmanager
@@ -38,15 +37,16 @@ def _make_cwd_env():
3837
return cloudpickle_repo_folder, env
3938

4039

41-
def subprocess_pickle_echo(input_data, protocol=None, timeout=TIMEOUT):
42-
"""Echo function with a child Python process
40+
def subprocess_pickle_string(input_data, protocol=None, timeout=TIMEOUT,
41+
add_env=None):
42+
"""Retrieve pickle string of an object generated by a child Python process
4343
4444
Pickle the input data into a buffer, send it to a subprocess via
4545
stdin, expect the subprocess to unpickle, re-pickle that data back
4646
and send it back to the parent process via stdout for final unpickling.
4747
48-
>>> subprocess_pickle_echo([1, 'a', None])
49-
[1, 'a', None]
48+
>>> testutils.subprocess_pickle_string([1, 'a', None], protocol=2)
49+
b'\x80\x02]q\x00(K\x01X\x01\x00\x00\x00aq\x01Ne.'
5050
5151
"""
5252
# run then pickle_echo(protocol=protocol) in __main__:
@@ -56,6 +56,8 @@ def subprocess_pickle_echo(input_data, protocol=None, timeout=TIMEOUT):
5656
# which is deprecated in python 3.8
5757
cmd = [sys.executable, '-W ignore', __file__, "--protocol", str(protocol)]
5858
cwd, env = _make_cwd_env()
59+
if add_env:
60+
env.update(add_env)
5961
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd, env=env,
6062
bufsize=4096)
6163
pickle_string = dumps(input_data, protocol=protocol)
@@ -67,14 +69,30 @@ def subprocess_pickle_echo(input_data, protocol=None, timeout=TIMEOUT):
6769
message = "Subprocess returned %d: " % proc.returncode
6870
message += err.decode('utf-8')
6971
raise RuntimeError(message)
70-
return loads(out)
72+
return out
7173
except TimeoutExpired as e:
7274
proc.kill()
7375
out, err = proc.communicate()
7476
message = u"\n".join([out.decode('utf-8'), err.decode('utf-8')])
7577
raise RuntimeError(message) from e
7678

7779

80+
def subprocess_pickle_echo(input_data, protocol=None, timeout=TIMEOUT,
81+
add_env=None):
82+
"""Echo function with a child Python process
83+
Pickle the input data into a buffer, send it to a subprocess via
84+
stdin, expect the subprocess to unpickle, re-pickle that data back
85+
and send it back to the parent process via stdout for final unpickling.
86+
>>> subprocess_pickle_echo([1, 'a', None])
87+
[1, 'a', None]
88+
"""
89+
out = subprocess_pickle_string(input_data,
90+
protocol=protocol,
91+
timeout=timeout,
92+
add_env=add_env)
93+
return loads(out)
94+
95+
7896
def _read_all_bytes(stream_in, chunk_size=4096):
7997
all_data = b""
8098
while True:

0 commit comments

Comments
 (0)