Hello community, here is the log from the commit of package python-cloudpickle for openSUSE:Factory checked in at 2019-04-02 09:21:32 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-cloudpickle (Old) and /work/SRC/openSUSE:Factory/.python-cloudpickle.new.25356 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cloudpickle" Tue Apr 2 09:21:32 2019 rev:5 rq:689381 version:0.8.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-cloudpickle/python-cloudpickle.changes 2019-02-06 15:48:19.751226078 +0100 +++ /work/SRC/openSUSE:Factory/.python-cloudpickle.new.25356/python-cloudpickle.changes 2019-04-02 09:21:46.028681327 +0200 @@ -1,0 +2,13 @@ +Thu Mar 28 14:20:54 UTC 2019 - Tomáš Chvátal <[email protected]> + +- Update to 0.8.1: + * Fix a bug (already present before 0.5.3 and re-introduced in 0.8.0) affecting relative import instructions inside depickled functions (issue #254) + +------------------------------------------------------------------- +Thu Mar 7 13:00:07 UTC 2019 - Tomáš Chvátal <[email protected]> + +- Update to 0.8.0: + * Add support for pickling interactively defined dataclasses. (issue #245) + * Global variables referenced by functions pickled by cloudpickle are now unpickled in a new and isolated namespace scoped by the CloudPickler instance. This restores the (previously untested) behavior of cloudpickle prior to changes done in 0.5.4 for functions defined in the __main__ module, and 0.6.0/1 for other dynamic functions. + +------------------------------------------------------------------- Old: ---- cloudpickle-0.7.0.tar.gz New: ---- cloudpickle-0.8.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-cloudpickle.spec ++++++ --- /var/tmp/diff_new_pack.87w4wu/_old 2019-04-02 09:21:47.204682428 +0200 +++ /var/tmp/diff_new_pack.87w4wu/_new 2019-04-02 09:21:47.208682432 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-cloudpickle -Version: 0.7.0 +Version: 0.8.1 Release: 0 Summary: Extended pickling support for Python objects License: BSD-3-Clause @@ -27,11 +27,13 @@ Source: https://files.pythonhosted.org/packages/source/c/cloudpickle/cloudpickle-%{version}.tar.gz BuildRequires: %{python_module setuptools} BuildRequires: fdupes +BuildRequires: python-futures BuildRequires: python-rpm-macros BuildArch: noarch BuildRequires: %{python_module curses} BuildRequires: %{python_module mock} BuildRequires: %{python_module numpy >= 1.8.2} +BuildRequires: %{python_module psutil} BuildRequires: %{python_module pytest-cov} BuildRequires: %{python_module pytest} BuildRequires: %{python_module scipy} @@ -65,7 +67,6 @@ %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -# Tests require very specific paths and py.test arguments export PYTHONPATH='.:tests' %python_expand py.test-%{$python_bin_suffix} -s ++++++ cloudpickle-0.7.0.tar.gz -> cloudpickle-0.8.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/PKG-INFO new/cloudpickle-0.8.1/PKG-INFO --- old/cloudpickle-0.7.0/PKG-INFO 2019-01-23 17:36:06.000000000 +0100 +++ new/cloudpickle-0.8.1/PKG-INFO 2019-03-25 10:07:23.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cloudpickle -Version: 0.7.0 +Version: 0.8.1 Summary: Extended pickling support for Python objects Home-page: https://github.com/cloudpipe/cloudpickle Author: Cloudpipe @@ -15,16 +15,20 @@ `cloudpickle` makes it possible to serialize Python constructs not supported by the default `pickle` module from the Python standard library. - `cloudpickle` is especially useful for cluster computing where Python - expressions are shipped over the network to execute on remote hosts, possibly - close to the data. - - Among other things, `cloudpickle` supports pickling for lambda expressions, - functions and classes defined interactively in the `__main__` module. - - `cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default: it is meant to - send objects between processes running the same version of Python. It is - discouraged to use `cloudpickle` for long-term storage. + `cloudpickle` is especially useful for **cluster computing** where Python + code is shipped over the network to execute on remote hosts, possibly close + to the data. + + Among other things, `cloudpickle` supports pickling for **lambda functions** + along with **functions and classes defined interactively** in the + `__main__` module (for instance in a script, a shell or a Jupyter notebook). + + **`cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default**: it is meant to + send objects between processes running the **same version of Python**. + + Using `cloudpickle` for **long-term object storage is not supported and + discouraged.** + Installation ------------ @@ -75,7 +79,7 @@ or alternatively for a specific environment: - tox -e py27 + tox -e py37 - With `py.test` to only run the tests for your current version of @@ -108,9 +112,9 @@ Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/README.md new/cloudpickle-0.8.1/README.md --- old/cloudpickle-0.7.0/README.md 2017-11-15 17:06:41.000000000 +0100 +++ new/cloudpickle-0.8.1/README.md 2019-02-19 14:29:43.000000000 +0100 @@ -7,16 +7,20 @@ `cloudpickle` makes it possible to serialize Python constructs not supported by the default `pickle` module from the Python standard library. -`cloudpickle` is especially useful for cluster computing where Python -expressions are shipped over the network to execute on remote hosts, possibly -close to the data. - -Among other things, `cloudpickle` supports pickling for lambda expressions, -functions and classes defined interactively in the `__main__` module. - -`cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default: it is meant to -send objects between processes running the same version of Python. It is -discouraged to use `cloudpickle` for long-term storage. +`cloudpickle` is especially useful for **cluster computing** where Python +code is shipped over the network to execute on remote hosts, possibly close +to the data. + +Among other things, `cloudpickle` supports pickling for **lambda functions** +along with **functions and classes defined interactively** in the +`__main__` module (for instance in a script, a shell or a Jupyter notebook). + +**`cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default**: it is meant to +send objects between processes running the **same version of Python**. + +Using `cloudpickle` for **long-term object storage is not supported and +discouraged.** + Installation ------------ @@ -67,7 +71,7 @@ or alternatively for a specific environment: - tox -e py27 + tox -e py37 - With `py.test` to only run the tests for your current version of diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/cloudpickle/__init__.py new/cloudpickle-0.8.1/cloudpickle/__init__.py --- old/cloudpickle-0.7.0/cloudpickle/__init__.py 2019-01-23 17:34:22.000000000 +0100 +++ new/cloudpickle-0.8.1/cloudpickle/__init__.py 2019-03-25 10:07:01.000000000 +0100 @@ -2,4 +2,4 @@ from cloudpickle.cloudpickle import * -__version__ = '0.7.0' +__version__ = '0.8.1' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/cloudpickle/cloudpickle.py new/cloudpickle-0.8.1/cloudpickle/cloudpickle.py --- old/cloudpickle-0.7.0/cloudpickle/cloudpickle.py 2019-01-23 17:32:14.000000000 +0100 +++ new/cloudpickle-0.8.1/cloudpickle/cloudpickle.py 2019-03-25 09:45:31.000000000 +0100 @@ -63,7 +63,7 @@ DEFAULT_PROTOCOL = pickle.HIGHEST_PROTOCOL -if sys.version < '3': +if sys.version_info[0] < 3: # pragma: no branch from pickle import Pickler try: from cStringIO import StringIO @@ -79,22 +79,6 @@ PY3 = True -# Container for the global namespace to ensure consistent unpickling of -# functions defined in dynamic modules (modules not registed in sys.modules). -_dynamic_modules_globals = weakref.WeakValueDictionary() - - -class _DynamicModuleFuncGlobals(dict): - """Global variables referenced by a function defined in a dynamic module - - To avoid leaking references we store such context in a WeakValueDictionary - instance. However instances of python builtin types such as dict cannot - be used directly as values in such a construct, hence the need for a - derived class. - """ - pass - - def _make_cell_set_template_code(): """Get the Python compiler to emit LOAD_FAST(arg); STORE_DEREF @@ -128,7 +112,7 @@ # NOTE: we are marking the cell variable as a free variable intentionally # so that we simulate an inner function instead of the outer function. This # is what gives us the ``nonlocal`` behavior in a Python 2 compatible way. - if not PY3: + if not PY3: # pragma: no branch return types.CodeType( co.co_argcount, co.co_nlocals, @@ -229,14 +213,14 @@ } -if sys.version_info < (3, 4): +if sys.version_info < (3, 4): # pragma: no branch def _walk_global_ops(code): """ Yield (opcode, argument number) tuples for all global-referencing instructions in *code*. """ code = getattr(code, 'co_code', b'') - if not PY3: + if not PY3: # pragma: no branch code = map(ord, code) n = len(code) @@ -293,7 +277,7 @@ dispatch[memoryview] = save_memoryview - if not PY3: + if not PY3: # pragma: no branch def save_buffer(self, obj): self.save(str(obj)) @@ -315,7 +299,7 @@ """ Save a code object """ - if PY3: + if PY3: # pragma: no branch args = ( obj.co_argcount, obj.co_kwonlyargcount, obj.co_nlocals, obj.co_stacksize, obj.co_flags, obj.co_code, obj.co_consts, obj.co_names, obj.co_varnames, @@ -393,7 +377,7 @@ # So we pickle them here using save_reduce; have to do it differently # for different python versions. if not hasattr(obj, '__code__'): - if PY3: + if PY3: # pragma: no branch rv = obj.__reduce_ex__(self.proto) else: if hasattr(obj, '__self__'): @@ -670,17 +654,26 @@ # save the dict dct = func.__dict__ - base_globals = self.globals_ref.get(id(func.__globals__), None) - if base_globals is None: - # For functions defined in a well behaved module use - # vars(func.__module__) for base_globals. This is necessary to - # share the global variables across multiple pickled functions from - # this module. - if func.__module__ is not None: - base_globals = func.__module__ - else: - base_globals = {} - self.globals_ref[id(func.__globals__)] = base_globals + # base_globals represents the future global namespace of func at + # unpickling time. Looking it up and storing it in globals_ref allow + # functions sharing the same globals at pickling time to also + # share them once unpickled, at one condition: since globals_ref is + # an attribute of a Cloudpickler instance, and that a new CloudPickler is + # created each time pickle.dump or pickle.dumps is called, functions + # also need to be saved within the same invokation of + # cloudpickle.dump/cloudpickle.dumps (for example: cloudpickle.dumps([f1, f2])). There + # is no such limitation when using Cloudpickler.dump, as long as the + # multiple invokations are bound to the same Cloudpickler. + base_globals = self.globals_ref.setdefault(id(func.__globals__), {}) + + if base_globals == {}: + # Add module attributes used to resolve relative imports + # instructions inside func. + for k in ["__package__", "__name__", "__path__", "__file__"]: + # Some built-in functions/methods such as object.__new__ have + # their __globals__ set to None in PyPy + if func.__globals__ is not None and k in func.__globals__: + base_globals[k] = func.__globals__[k] return (code, f_globals, defaults, closure, dct, base_globals) @@ -730,7 +723,7 @@ if obj.__self__ is None: self.save_reduce(getattr, (obj.im_class, obj.__name__)) else: - if PY3: + if PY3: # pragma: no branch self.save_reduce(types.MethodType, (obj.__func__, obj.__self__), obj=obj) else: self.save_reduce(types.MethodType, (obj.__func__, obj.__self__, obj.__self__.__class__), @@ -783,7 +776,7 @@ save(stuff) write(pickle.BUILD) - if not PY3: + if not PY3: # pragma: no branch dispatch[types.InstanceType] = save_inst def save_property(self, obj): @@ -883,7 +876,7 @@ try: # Python 2 dispatch[file] = save_file - except NameError: # Python 3 + except NameError: # Python 3 # pragma: no branch dispatch[io.TextIOWrapper] = save_file dispatch[type(Ellipsis)] = save_ellipsis @@ -904,6 +897,12 @@ dispatch[logging.RootLogger] = save_root_logger + if hasattr(types, "MappingProxyType"): # pragma: no branch + def save_mappingproxy(self, obj): + self.save_reduce(types.MappingProxyType, (dict(obj),), obj=obj) + + dispatch[types.MappingProxyType] = save_mappingproxy + """Special functions for Add-on libraries""" def inject_addons(self): """Plug in system. Register additional pickling functions if modules already loaded""" @@ -989,43 +988,6 @@ return obj -def _get_module_builtins(): - return pickle.__builtins__ - - -def print_exec(stream): - ei = sys.exc_info() - traceback.print_exception(ei[0], ei[1], ei[2], None, stream) - - -def _modules_to_main(modList): - """Force every module in modList to be placed into main""" - if not modList: - return - - main = sys.modules['__main__'] - for modname in modList: - if type(modname) is str: - try: - mod = __import__(modname) - except Exception: - sys.stderr.write('warning: could not import %s\n. ' - 'Your function may unexpectedly error due to this import failing;' - 'A version mismatch is likely. Specific error was:\n' % modname) - print_exec(sys.stderr) - else: - setattr(main, mod.__name__, mod) - - -# object generators: -def _genpartial(func, args, kwds): - if not args: - args = () - if not kwds: - kwds = {} - return partial(func, *args, **kwds) - - def _gen_ellipsis(): return Ellipsis @@ -1090,10 +1052,16 @@ else: raise ValueError('Unexpected _fill_value arguments: %r' % (args,)) - # Only set global variables that do not exist. - for k, v in state['globals'].items(): - if k not in func.__globals__: - func.__globals__[k] = v + # - At pickling time, any dynamic global variable used by func is + # serialized by value (in state['globals']). + # - At unpickling time, func's __globals__ attribute is initialized by + # first retrieving an empty isolated namespace that will be shared + # with other functions pickled from the same original module + # by the same CloudPickler instance and then updated with the + # content of state['globals'] to populate the shared isolated + # namespace with all the global variables that are specifically + # referenced for this function. + func.__globals__.update(state['globals']) func.__defaults__ = state['defaults'] func.__dict__ = state['dict'] @@ -1131,21 +1099,11 @@ code and the correct number of cells in func_closure. All other func attributes (e.g. func_globals) are empty. """ - if base_globals is None: + # This is backward-compatibility code: for cloudpickle versions between + # 0.5.4 and 0.7, base_globals could be a string or None. base_globals + # should now always be a dictionary. + if base_globals is None or isinstance(base_globals, str): base_globals = {} - elif isinstance(base_globals, str): - base_globals_name = base_globals - try: - # First try to reuse the globals from the module containing the - # function. If it is not possible to retrieve it, fallback to an - # empty dictionary. - base_globals = vars(importlib.import_module(base_globals)) - except ImportError: - base_globals = _dynamic_modules_globals.get( - base_globals_name, None) - if base_globals is None: - base_globals = _DynamicModuleFuncGlobals() - _dynamic_modules_globals[base_globals_name] = base_globals base_globals['__builtins__'] = __builtins__ @@ -1202,18 +1160,9 @@ return False -"""Constructors for 3rd party libraries -Note: These can never be renamed due to client compatibility issues""" - - -def _getobject(modname, attribute): - mod = __import__(modname, fromlist=[attribute]) - return mod.__dict__[attribute] - - """ Use copy_reg to extend global pickle definitions """ -if sys.version_info < (3, 4): +if sys.version_info < (3, 4): # pragma: no branch method_descriptor = type(str.upper) def _reduce_method_descriptor(obj): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/cloudpickle.egg-info/PKG-INFO new/cloudpickle-0.8.1/cloudpickle.egg-info/PKG-INFO --- old/cloudpickle-0.7.0/cloudpickle.egg-info/PKG-INFO 2019-01-23 17:36:05.000000000 +0100 +++ new/cloudpickle-0.8.1/cloudpickle.egg-info/PKG-INFO 2019-03-25 10:07:23.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: cloudpickle -Version: 0.7.0 +Version: 0.8.1 Summary: Extended pickling support for Python objects Home-page: https://github.com/cloudpipe/cloudpickle Author: Cloudpipe @@ -15,16 +15,20 @@ `cloudpickle` makes it possible to serialize Python constructs not supported by the default `pickle` module from the Python standard library. - `cloudpickle` is especially useful for cluster computing where Python - expressions are shipped over the network to execute on remote hosts, possibly - close to the data. - - Among other things, `cloudpickle` supports pickling for lambda expressions, - functions and classes defined interactively in the `__main__` module. - - `cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default: it is meant to - send objects between processes running the same version of Python. It is - discouraged to use `cloudpickle` for long-term storage. + `cloudpickle` is especially useful for **cluster computing** where Python + code is shipped over the network to execute on remote hosts, possibly close + to the data. + + Among other things, `cloudpickle` supports pickling for **lambda functions** + along with **functions and classes defined interactively** in the + `__main__` module (for instance in a script, a shell or a Jupyter notebook). + + **`cloudpickle` uses `pickle.HIGHEST_PROTOCOL` by default**: it is meant to + send objects between processes running the **same version of Python**. + + Using `cloudpickle` for **long-term object storage is not supported and + discouraged.** + Installation ------------ @@ -75,7 +79,7 @@ or alternatively for a specific environment: - tox -e py27 + tox -e py37 - With `py.test` to only run the tests for your current version of @@ -108,9 +112,9 @@ Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/cloudpickle.egg-info/SOURCES.txt new/cloudpickle-0.8.1/cloudpickle.egg-info/SOURCES.txt --- old/cloudpickle-0.7.0/cloudpickle.egg-info/SOURCES.txt 2019-01-23 17:36:05.000000000 +0100 +++ new/cloudpickle-0.8.1/cloudpickle.egg-info/SOURCES.txt 2019-03-25 10:07:23.000000000 +0100 @@ -12,4 +12,6 @@ tests/__init__.py tests/cloudpickle_file_test.py tests/cloudpickle_test.py -tests/testutils.py \ No newline at end of file +tests/testutils.py +tests/mypkg/__init__.py +tests/mypkg/mod.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/setup.py new/cloudpickle-0.8.1/setup.py --- old/cloudpickle-0.7.0/setup.py 2019-01-23 17:29:10.000000000 +0100 +++ new/cloudpickle-0.8.1/setup.py 2019-01-31 14:05:30.000000000 +0100 @@ -39,9 +39,9 @@ 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/tests/cloudpickle_test.py new/cloudpickle-0.8.1/tests/cloudpickle_test.py --- old/cloudpickle-0.7.0/tests/cloudpickle_test.py 2019-01-23 10:51:18.000000000 +0100 +++ new/cloudpickle-0.8.1/tests/cloudpickle_test.py 2019-03-25 09:45:31.000000000 +0100 @@ -451,57 +451,6 @@ mod1, mod2 = pickle_depickle([mod, mod]) self.assertEqual(id(mod1), id(mod2)) - def test_dynamic_modules_globals(self): - # _dynamic_modules_globals is a WeakValueDictionary, so if a value - # in this dict (containing a set of global variables from a dynamic - # module created in the parent process) has no other reference than in - # this dict in the child process, it will be garbage collected. - - # We first create a module - mod = types.ModuleType('mod') - code = ''' - x = 1 - def func(): - return - ''' - exec(textwrap.dedent(code), mod.__dict__) - - pickled_module_path = os.path.join(self.tmpdir, 'mod_f.pkl') - child_process_script = ''' - import pickle - from cloudpickle.cloudpickle import _dynamic_modules_globals - import gc - with open("{pickled_module_path}", 'rb') as f: - func = pickle.load(f) - - # A dictionnary storing the globals of the newly unpickled function - # should have been created - assert list(_dynamic_modules_globals.keys()) == ['mod'] - - # func.__globals__ is the only non-weak reference to - # _dynamic_modules_globals['mod']. By deleting func, we delete also - # _dynamic_modules_globals['mod'] - del func - gc.collect() - - # There is no reference to the globals of func since func has been - # deleted and _dynamic_modules_globals is a WeakValueDictionary, - # so _dynamic_modules_globals should now be empty - assert list(_dynamic_modules_globals.keys()) == [] - ''' - - child_process_script = child_process_script.format( - pickled_module_path=_escape(pickled_module_path)) - - try: - with open(pickled_module_path, 'wb') as f: - cloudpickle.dump(mod.func, f, protocol=self.protocol) - - assert_run_python_script(textwrap.dedent(child_process_script)) - - finally: - os.unlink(pickled_module_path) - def test_module_locals_behavior(self): # Makes sure that a local function defined in another module is # correctly serialized. This notably checks that the globals are @@ -1029,6 +978,12 @@ def f4(x): return foo.method(x) + def f5(x): + # Recursive call to a dynamically defined function. + if x <= 0: + return f4(x) + return f5(x - 1) + 1 + cloned = subprocess_pickle_echo(lambda x: x**2, protocol={protocol}) assert cloned(3) == 9 @@ -1052,6 +1007,9 @@ cloned = subprocess_pickle_echo(f4, protocol={protocol}) assert cloned(2) == f4(2) + + cloned = subprocess_pickle_echo(f5, protocol={protocol}) + assert cloned(7) == f5(7) == 7 """.format(protocol=self.protocol) assert_run_python_script(textwrap.dedent(code)) @@ -1074,20 +1032,42 @@ def f1(): return VARIABLE - cloned_f0 = {clone_func}(f0, protocol={protocol}) - cloned_f1 = {clone_func}(f1, protocol={protocol}) + assert f0.__globals__ is f1.__globals__ + + # pickle f0 and f1 inside the same pickle_string + cloned_f0, cloned_f1 = {clone_func}([f0, f1], protocol={protocol}) + + # cloned_f0 and cloned_f1 now share a global namespace that is isolated + # from any previously existing namespace + assert cloned_f0.__globals__ is cloned_f1.__globals__ + assert cloned_f0.__globals__ is not f0.__globals__ + + # pickle f1 another time, but in a new pickle string pickled_f1 = dumps(f1, protocol={protocol}) - # Change the value of the global variable + # Change the value of the global variable in f0's new global namespace cloned_f0() - # Ensure that the global variable is the same for another function - result_f1 = cloned_f1() - assert result_f1 == "changed_by_f0", result_f1 - - # Ensure that unpickling the global variable does not change its value - result_pickled_f1 = loads(pickled_f1)() - assert result_pickled_f1 == "changed_by_f0", result_pickled_f1 + # thanks to cloudpickle isolation, depickling and calling f0 and f1 + # should not affect the globals of already existing modules + assert VARIABLE == "default_value", VARIABLE + + # Ensure that cloned_f1 and cloned_f0 share the same globals, as f1 and + # f0 shared the same globals at pickling time, and cloned_f1 was + # depickled from the same pickle string as cloned_f0 + shared_global_var = cloned_f1() + assert shared_global_var == "changed_by_f0", shared_global_var + + # f1 is unpickled another time, but because it comes from another + # pickle string than pickled_f1 and pickled_f0, it will not share the + # same globals as the latter two. + new_cloned_f1 = loads(pickled_f1) + assert new_cloned_f1.__globals__ is not cloned_f1.__globals__ + assert new_cloned_f1.__globals__ is not f1.__globals__ + + # get the value of new_cloned_f1's VARIABLE + new_global_var = new_cloned_f1() + assert new_global_var == "default_value", new_global_var """ for clone_func in ['local_clone', 'subprocess_pickle_echo']: code = code_template.format(protocol=self.protocol, @@ -1106,116 +1086,154 @@ def f1(): return _TEST_GLOBAL_VARIABLE - cloned_f0 = cloudpickle.loads(cloudpickle.dumps( - f0, protocol=self.protocol)) - cloned_f1 = cloudpickle.loads(cloudpickle.dumps( - f1, protocol=self.protocol)) + # pickle f0 and f1 inside the same pickle_string + cloned_f0, cloned_f1 = pickle_depickle([f0, f1], + protocol=self.protocol) + + # cloned_f0 and cloned_f1 now share a global namespace that is + # isolated from any previously existing namespace + assert cloned_f0.__globals__ is cloned_f1.__globals__ + assert cloned_f0.__globals__ is not f0.__globals__ + + # pickle f1 another time, but in a new pickle string pickled_f1 = cloudpickle.dumps(f1, protocol=self.protocol) - # Change the value of the global variable + # Change the global variable's value in f0's new global namespace cloned_f0() - assert _TEST_GLOBAL_VARIABLE == "changed_by_f0" - # Ensure that the global variable is the same for another function - result_cloned_f1 = cloned_f1() - assert result_cloned_f1 == "changed_by_f0", result_cloned_f1 - assert f1() == result_cloned_f1 - - # Ensure that unpickling the global variable does not change its - # value - result_pickled_f1 = cloudpickle.loads(pickled_f1)() - assert result_pickled_f1 == "changed_by_f0", result_pickled_f1 + # depickling f0 and f1 should not affect the globals of already + # existing modules + assert _TEST_GLOBAL_VARIABLE == "default_value" + + # Ensure that cloned_f1 and cloned_f0 share the same globals, as f1 + # and f0 shared the same globals at pickling time, and cloned_f1 + # was depickled from the same pickle string as cloned_f0 + shared_global_var = cloned_f1() + assert shared_global_var == "changed_by_f0", shared_global_var + + # f1 is unpickled another time, but because it comes from another + # pickle string than pickled_f1 and pickled_f0, it will not share + # the same globals as the latter two. + new_cloned_f1 = pickle.loads(pickled_f1) + assert new_cloned_f1.__globals__ is not cloned_f1.__globals__ + assert new_cloned_f1.__globals__ is not f1.__globals__ + + # get the value of new_cloned_f1's VARIABLE + new_global_var = new_cloned_f1() + assert new_global_var == "default_value", new_global_var finally: _TEST_GLOBAL_VARIABLE = orig_value - def test_function_from_dynamic_module_with_globals_modifications(self): - # This test verifies that the global variable state of a function - # defined in a dynamic module in a child process are not reset by - # subsequent uplickling. + def test_interactive_remote_function_calls(self): + code = """if __name__ == "__main__": + from testutils import subprocess_worker - # first, we create a dynamic module in the parent process - mod = types.ModuleType('mod') - code = ''' - GLOBAL_STATE = "initial value" + def interactive_function(x): + return x + 1 - def func_defined_in_dynamic_module(v=None): - global GLOBAL_STATE - if v is not None: - GLOBAL_STATE = v - return GLOBAL_STATE - ''' - exec(textwrap.dedent(code), mod.__dict__) + with subprocess_worker(protocol={protocol}) as w: - with_initial_globals_file = os.path.join( - self.tmpdir, 'function_with_initial_globals.pkl') - with_modified_globals_file = os.path.join( - self.tmpdir, 'function_with_modified_globals.pkl') + assert w.run(interactive_function, 41) == 42 - try: - # Simple sanity check on the function's output - assert mod.func_defined_in_dynamic_module() == "initial value" + # Define a new function that will call an updated version of + # the previously called function: - # The function of mod is pickled two times, with two different - # values for the global variable GLOBAL_STATE. - # Then we launch a child process that sequentially unpickles the - # two functions. Those unpickle functions should share the same - # global variables in the child process: - # Once the first function gets unpickled, mod is created and - # tracked in the child environment. This is state is preserved - # when unpickling the second function whatever the global variable - # GLOBAL_STATE's value at the time of pickling. - - with open(with_initial_globals_file, 'wb') as f: - cloudpickle.dump(mod.func_defined_in_dynamic_module, f) - - # Change the mod's global variable - mod.GLOBAL_STATE = 'changed value' - - # At this point, mod.func_defined_in_dynamic_module() - # returns the updated value. Let's pickle it again. - assert mod.func_defined_in_dynamic_module() == 'changed value' - with open(with_modified_globals_file, 'wb') as f: - cloudpickle.dump(mod.func_defined_in_dynamic_module, f, - protocol=self.protocol) + def wrapper_func(x): + return interactive_function(x) - child_process_code = """ - import pickle + def interactive_function(x): + return x - 1 + + # The change in the definition of interactive_function in the main + # module of the main process should be reflected transparently + # in the worker process: the worker process does not recall the + # previous definition of `interactive_function`: - with open({with_initial_globals_file!r},'rb') as f: - func_with_initial_globals = pickle.load(f) + assert w.run(wrapper_func, 41) == 40 + """.format(protocol=self.protocol) + assert_run_python_script(code) - # At this point, a module called 'mod' should exist in - # _dynamic_modules_globals. Further function loading - # will use the globals living in mod. - - assert func_with_initial_globals() == 'initial value' - - # Load a function with initial global variable that was - # pickled after a change in the global variable - with open({with_initial_globals_file!r},'rb') as f: - func_with_modified_globals = pickle.load(f) - - # assert the this unpickling did not modify the value of - # the local - assert func_with_modified_globals() == 'initial value' - - # Update the value from the child process and check that - # unpickling again does not reset our change. - assert func_with_initial_globals('new value') == 'new value' - assert func_with_modified_globals() == 'new value' - - with open({with_initial_globals_file!r},'rb') as f: - func_with_initial_globals = pickle.load(f) - assert func_with_initial_globals() == 'new value' - assert func_with_modified_globals() == 'new value' - """.format( - with_initial_globals_file=_escape(with_initial_globals_file), - with_modified_globals_file=_escape(with_modified_globals_file)) - assert_run_python_script(textwrap.dedent(child_process_code)) + def test_interactive_remote_function_calls_no_side_effect(self): + code = """if __name__ == "__main__": + from testutils import subprocess_worker + import sys - finally: - os.unlink(with_initial_globals_file) - os.unlink(with_modified_globals_file) + with subprocess_worker(protocol={protocol}) as w: + + GLOBAL_VARIABLE = 0 + + class CustomClass(object): + + def mutate_globals(self): + global GLOBAL_VARIABLE + GLOBAL_VARIABLE += 1 + return GLOBAL_VARIABLE + + custom_object = CustomClass() + assert w.run(custom_object.mutate_globals) == 1 + + # The caller global variable is unchanged in the main process. + + assert GLOBAL_VARIABLE == 0 + + # Calling the same function again starts again from zero. The + # worker process is stateless: it has no memory of the past call: + + assert w.run(custom_object.mutate_globals) == 1 + + # The symbols defined in the main process __main__ module are + # not set in the worker process main module to leave the worker + # as stateless as possible: + + def is_in_main(name): + return hasattr(sys.modules["__main__"], name) + + assert is_in_main("CustomClass") + assert not w.run(is_in_main, "CustomClass") + + assert is_in_main("GLOBAL_VARIABLE") + assert not w.run(is_in_main, "GLOBAL_VARIABLE") + + """.format(protocol=self.protocol) + assert_run_python_script(code) + + @pytest.mark.skipif(platform.python_implementation() == 'PyPy', + reason="Skip PyPy because memory grows too much") + def test_interactive_remote_function_calls_no_memory_leak(self): + code = """if __name__ == "__main__": + from testutils import subprocess_worker + import struct + + with subprocess_worker(protocol={protocol}) as w: + + reference_size = w.memsize() + assert reference_size > 0 + + + def make_big_closure(i): + # Generate a byte string of size 1MB + itemsize = len(struct.pack("l", 1)) + data = struct.pack("l", i) * (int(1e6) // itemsize) + def process_data(): + return len(data) + return process_data + + for i in range(100): + func = make_big_closure(i) + result = w.run(func) + assert result == int(1e6), result + + import gc + w.run(gc.collect) + + # By this time the worker process has processed worth of 100MB of + # data passed in the closures its memory size should now have + # grown by more than a few MB. + growth = w.memsize() - reference_size + assert growth < 1e7, growth + + """.format(protocol=self.protocol) + assert_run_python_script(code) @pytest.mark.skipif(sys.version_info >= (3, 0), reason="hardcoded pickle bytes for 2.7") @@ -1335,6 +1353,45 @@ with pytest.raises(AttributeError): obj.non_registered_attribute = 1 + @unittest.skipIf(not hasattr(types, "MappingProxyType"), + "Old versions of Python do not have this type.") + def test_mappingproxy(self): + mp = types.MappingProxyType({"some_key": "some value"}) + assert mp == pickle_depickle(mp, protocol=self.protocol) + + def test_dataclass(self): + dataclasses = pytest.importorskip("dataclasses") + + DataClass = dataclasses.make_dataclass('DataClass', [('x', int)]) + data = DataClass(x=42) + + pickle_depickle(DataClass, protocol=self.protocol) + assert data.x == pickle_depickle(data, protocol=self.protocol).x == 42 + + def test_relative_import_inside_function(self): + # Make sure relative imports inside round-tripped functions is not + # broken.This was a bug in cloudpickle versions <= 0.5.3 and was + # re-introduced in 0.8.0. + + # Both functions living inside modules and packages are tested. + def f(): + # module_function belongs to mypkg.mod1, which is a module + from .mypkg import module_function + return module_function() + + def g(): + # package_function belongs to mypkg, which is a package + from .mypkg import package_function + return package_function() + + for func, source in zip([f, g], ["module", "package"]): + # Make sure relative imports are initially working + assert func() == "hello from a {}!".format(source) + + # Make sure relative imports still work after round-tripping + cloned_func = pickle_depickle(func, protocol=self.protocol) + assert cloned_func() == "hello from a {}!".format(source) + class Protocol2CloudPickleTest(CloudPickleTest): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/tests/mypkg/__init__.py new/cloudpickle-0.8.1/tests/mypkg/__init__.py --- old/cloudpickle-0.7.0/tests/mypkg/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/cloudpickle-0.8.1/tests/mypkg/__init__.py 2019-03-25 09:45:31.000000000 +0100 @@ -0,0 +1,6 @@ +from .mod import module_function + + +def package_function(): + """Function living inside a package, not a simple module""" + return "hello from a package!" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/tests/mypkg/mod.py new/cloudpickle-0.8.1/tests/mypkg/mod.py --- old/cloudpickle-0.7.0/tests/mypkg/mod.py 1970-01-01 01:00:00.000000000 +0100 +++ new/cloudpickle-0.8.1/tests/mypkg/mod.py 2019-03-25 09:45:31.000000000 +0100 @@ -0,0 +1,2 @@ +def module_function(): + return "hello from a module!" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cloudpickle-0.7.0/tests/testutils.py new/cloudpickle-0.8.1/tests/testutils.py --- old/cloudpickle-0.7.0/tests/testutils.py 2019-01-23 16:35:51.000000000 +0100 +++ new/cloudpickle-0.8.1/tests/testutils.py 2019-02-19 14:29:43.000000000 +0100 @@ -4,8 +4,12 @@ import tempfile import base64 from subprocess import Popen, check_output, PIPE, STDOUT, CalledProcessError -from cloudpickle import dumps from pickle import loads +from contextlib import contextmanager +from concurrent.futures import ProcessPoolExecutor + +import psutil +from cloudpickle import dumps TIMEOUT = 60 try: @@ -42,6 +46,20 @@ return cloudpickle_repo_folder, env +def _pack(input_data, protocol=None): + pickled_input_data = dumps(input_data, protocol=protocol) + # Under Windows + Python 2.7, subprocess / communicate truncate the data + # on some specific bytes. To avoid this issue, let's use the pure ASCII + # Base32 encoding to encapsulate the pickle message sent to the child + # process. + return base64.b32encode(pickled_input_data) + + +def _unpack(packed_data): + decoded_data = base64.b32decode(packed_data) + return loads(decoded_data) + + def subprocess_pickle_echo(input_data, protocol=None, timeout=TIMEOUT): """Echo function with a child Python process @@ -53,18 +71,12 @@ [1, 'a', None] """ - pickled_input_data = dumps(input_data, protocol=protocol) - # Under Windows + Python 2.7, subprocess / communicate truncate the data - # on some specific bytes. To avoid this issue, let's use the pure ASCII - # Base32 encoding to encapsulate the pickle message sent to the child - # process. - pickled_b32 = base64.b32encode(pickled_input_data) - # run then pickle_echo(protocol=protocol) in __main__: cmd = [sys.executable, __file__, "--protocol", str(protocol)] cwd, env = _make_cwd_env() proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd, env=env, bufsize=4096) + pickled_b32 = _pack(input_data, protocol=protocol) try: comm_kwargs = {} if timeout_supported: @@ -74,7 +86,7 @@ message = "Subprocess returned %d: " % proc.returncode message += err.decode('utf-8') raise RuntimeError(message) - return loads(base64.b32decode(out)) + return _unpack(out) except TimeoutExpired: proc.kill() out, err = proc.communicate() @@ -113,6 +125,56 @@ stream_out.close() +def call_func(payload, protocol): + """Remote function call that uses cloudpickle to transport everthing""" + func, args, kwargs = loads(payload) + try: + result = func(*args, **kwargs) + except BaseException as e: + result = e + return dumps(result, protocol=protocol) + + +class _Worker(object): + def __init__(self, protocol=None): + self.protocol = protocol + self.pool = ProcessPoolExecutor(max_workers=1) + self.pool.submit(id, 42).result() # start the worker process + + def run(self, func, *args, **kwargs): + """Synchronous remote function call""" + + input_payload = dumps((func, args, kwargs), protocol=self.protocol) + result_payload = self.pool.submit( + call_func, input_payload, self.protocol).result() + result = loads(result_payload) + + if isinstance(result, BaseException): + raise result + return result + + def memsize(self): + workers_pids = [p.pid if hasattr(p, "pid") else p + for p in list(self.pool._processes)] + num_workers = len(workers_pids) + if num_workers == 0: + return 0 + elif num_workers > 1: + raise RuntimeError("Unexpected number of workers: %d" + % num_workers) + return psutil.Process(workers_pids[0]).memory_info().rss + + def close(self): + self.pool.shutdown(wait=True) + + +@contextmanager +def subprocess_worker(protocol=None): + worker = _Worker(protocol=protocol) + yield worker + worker.close() + + def assert_run_python_script(source_code, timeout=TIMEOUT): """Utility to help check pickleability of objects defined in __main__
