Skip to content

Commit b9dc17f

Browse files
pierreglaserogrisel
authored andcommitted
Support pickling of functions with positional-only arguments (#269)
1 parent a11b718 commit b9dc17f

4 files changed

Lines changed: 114 additions & 37 deletions

File tree

‎.travis.yml‎

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ matrix:
1212
- os: linux
1313
dist: trusty
1414
python: "pypy3"
15+
- os: linux
16+
if: commit_message =~ /(\[ci python-nightly\])/
17+
env: PYTHON_NIGHTLY=1
1518
- os: linux
1619
python: 3.7
1720
- os: linux
@@ -69,38 +72,53 @@ before_install:
6972
export PATH="$PYTHON_ROOT:$PYTHON_ROOT/Scripts:$PATH";
7073
python -m pip install --upgrade pip;
7174
fi
75+
- if [[ "$PYTHON_NIGHTLY" == 1 ]]; then
76+
export VENV_DIR="$HOME/python38";
77+
pushd ..;
78+
git clone https://github.com/python/cpython.git;
79+
pushd cpython;
80+
./configure;
81+
make;
82+
./python -m venv "$VENV_DIR";
83+
popd;
84+
popd;
85+
export PYTHON_EXE="$VENV_DIR/bin/python";
86+
else
87+
export PYTHON_EXE="python";
88+
fi
89+
7290
install:
73-
- pip install .
74-
- pip install --upgrade -r dev-requirements.txt
75-
- pip install tornado
76-
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then
77-
pip install numpy scipy;
91+
- $PYTHON_EXE -m pip install .
92+
- $PYTHON_EXE -m pip install --upgrade -r dev-requirements.txt
93+
- $PYTHON_EXE -m pip install tornado
94+
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* && "$PYTHON_NIGHTLY" != 1 ]]; then
95+
$PYTHON_EXE -m pip install numpy scipy;
7896
fi
7997
- if [[ $PROJECT != "" ]]; then
80-
pip install $TEST_REQUIREMENTS;
98+
$PYTHON_EXE -m pip install $TEST_REQUIREMENTS;
8199
pushd ..;
82100
git clone $PROJECT_URL;
83101
if [[ $PROJECT == "joblib" ]]; then
84102
pushd joblib/joblib/externals;
85103
source vendor_cloudpickle.sh ../../../cloudpickle;
86104
popd;
87105
fi;
88-
pip install ./$PROJECT;
106+
$PYTHON_EXE -m pip install ./$PROJECT;
89107
popd;
90108
fi
91-
- pip list
109+
- $PYTHON_EXE -m pip list
92110
before_script:
93111
# stop the build if there are Python syntax errors or undefined names
94-
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
112+
- $PYTHON_EXE -m flake8 . --count --verbose --select=E901,E999,F821,F822,F823 --show-source --statistics
95113
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
96-
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
97-
- python ci/install_coverage_subprocess_pth.py
114+
- $PYTHON_EXE -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
115+
- $PYTHON_EXE ci/install_coverage_subprocess_pth.py
98116
script:
99-
- COVERAGE_PROCESS_START="$TRAVIS_BUILD_DIR/.coveragerc" PYTHONPATH='.:tests' pytest -r s
117+
- COVERAGE_PROCESS_START="$TRAVIS_BUILD_DIR/.coveragerc" PYTHONPATH='.:tests' $PYTHON_EXE -m pytest -r s
100118
- |
101119
if [[ $PROJECT != "" ]]; then
102120
pushd ../$PROJECT
103-
pytest -vl
121+
$PYTHON_EXE -m pytest -vl
104122
TEST_RETURN_CODE=$?
105123
popd
106124
if [[ "$TEST_RETURN_CODE" != "0" ]]; then

‎CHANGES.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
1.1.0
22
=====
33

4+
- Support the pickling of interactively-defined functions with positional-only
5+
arguments. ([issue #266](https://github.com/cloudpipe/cloudpickle/pull/266))
6+
47
- Track the provenance of dynamic classes and enums so as to preseve the
58
usual `isinstance` relationship between pickled objects and their
69
original class defintions.

‎cloudpickle/cloudpickle.py‎

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,24 +168,43 @@ def inner(value):
168168
(),
169169
)
170170
else:
171-
return types.CodeType(
172-
co.co_argcount,
173-
co.co_kwonlyargcount,
174-
co.co_nlocals,
175-
co.co_stacksize,
176-
co.co_flags,
177-
co.co_code,
178-
co.co_consts,
179-
co.co_names,
180-
co.co_varnames,
181-
co.co_filename,
182-
co.co_name,
183-
co.co_firstlineno,
184-
co.co_lnotab,
185-
co.co_cellvars, # this is the trickery
186-
(),
187-
)
188-
171+
if hasattr(types.CodeType, "co_posonlyargcount"): # pragma: no branch
172+
return types.CodeType(
173+
co.co_argcount,
174+
co.co_posonlyargcount, # Python3.8 with PEP570
175+
co.co_kwonlyargcount,
176+
co.co_nlocals,
177+
co.co_stacksize,
178+
co.co_flags,
179+
co.co_code,
180+
co.co_consts,
181+
co.co_names,
182+
co.co_varnames,
183+
co.co_filename,
184+
co.co_name,
185+
co.co_firstlineno,
186+
co.co_lnotab,
187+
co.co_cellvars, # this is the trickery
188+
(),
189+
)
190+
else:
191+
return types.CodeType(
192+
co.co_argcount,
193+
co.co_kwonlyargcount,
194+
co.co_nlocals,
195+
co.co_stacksize,
196+
co.co_flags,
197+
co.co_code,
198+
co.co_consts,
199+
co.co_names,
200+
co.co_varnames,
201+
co.co_filename,
202+
co.co_name,
203+
co.co_firstlineno,
204+
co.co_lnotab,
205+
co.co_cellvars, # this is the trickery
206+
(),
207+
)
189208

190209
_cell_set_template_code = _make_cell_set_template_code()
191210

@@ -371,12 +390,23 @@ def save_codeobject(self, obj):
371390
Save a code object
372391
"""
373392
if PY3: # pragma: no branch
374-
args = (
375-
obj.co_argcount, obj.co_kwonlyargcount, obj.co_nlocals, obj.co_stacksize,
376-
obj.co_flags, obj.co_code, obj.co_consts, obj.co_names, obj.co_varnames,
377-
obj.co_filename, obj.co_name, obj.co_firstlineno, obj.co_lnotab, obj.co_freevars,
378-
obj.co_cellvars
379-
)
393+
if hasattr(obj, "co_posonlyargcount"): # pragma: no branch
394+
args = (
395+
obj.co_argcount, obj.co_posonlyargcount,
396+
obj.co_kwonlyargcount, obj.co_nlocals, obj.co_stacksize,
397+
obj.co_flags, obj.co_code, obj.co_consts, obj.co_names,
398+
obj.co_varnames, obj.co_filename, obj.co_name,
399+
obj.co_firstlineno, obj.co_lnotab, obj.co_freevars,
400+
obj.co_cellvars
401+
)
402+
else:
403+
args = (
404+
obj.co_argcount, obj.co_kwonlyargcount, obj.co_nlocals,
405+
obj.co_stacksize, obj.co_flags, obj.co_code, obj.co_consts,
406+
obj.co_names, obj.co_varnames, obj.co_filename,
407+
obj.co_name, obj.co_firstlineno, obj.co_lnotab,
408+
obj.co_freevars, obj.co_cellvars
409+
)
380410
else:
381411
args = (
382412
obj.co_argcount, obj.co_nlocals, obj.co_stacksize, obj.co_flags, obj.co_code,

‎tests/cloudpickle_test.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,32 @@ def f(a, *, b=1):
16391639
""".format(protocol=self.protocol)
16401640
assert_run_python_script(textwrap.dedent(code))
16411641

1642+
@pytest.mark.skipif(not hasattr(types.CodeType, "co_posonlyargcount"),
1643+
reason="Requires positional-only argument syntax")
1644+
def test_interactively_defined_func_with_positional_only_argument(self):
1645+
# Fixes https://github.com/cloudpipe/cloudpickle/issues/266
1646+
# The source code of this test is bundled in a string and is ran from
1647+
# the __main__ module of a subprocess in order to avoid a SyntaxError
1648+
# in versions of python that do not support positional-only argument
1649+
# syntax.
1650+
code = """
1651+
import pytest
1652+
from cloudpickle import loads, dumps
1653+
1654+
def f(a, /, b=1):
1655+
return a + b
1656+
1657+
depickled_f = loads(dumps(f, protocol={protocol}))
1658+
1659+
for func in (f, depickled_f):
1660+
assert func(2) == 3
1661+
assert func.__code__.co_posonlyargcount == 1
1662+
with pytest.raises(TypeError):
1663+
func(a=2)
1664+
1665+
""".format(protocol=self.protocol)
1666+
assert_run_python_script(textwrap.dedent(code))
1667+
16421668
class Protocol2CloudPickleTest(CloudPickleTest):
16431669

16441670
protocol = 2

0 commit comments

Comments
 (0)