From b20e7bd4668c73606282d15d60d0f4d3e6568c80 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Wed, 8 Nov 2017 17:00:55 +0100 Subject: [PATCH 1/2] Preserve func.__qualname__ when defined --- cloudpickle/cloudpickle.py | 57 +++++++++++++++++++++++++------------- tests/cloudpickle_test.py | 12 ++++++++ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 1d9861d21..4ce66ce75 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -529,11 +529,16 @@ def save_function_tuple(self, func): self.memoize(func) # save the rest of the func data needed by _fill_function - save(f_globals) - save(defaults) - save(dct) - save(func.__module__) - save(closure_values) + state = { + 'globals': f_globals, + 'defaults': defaults, + 'dict': dct, + 'module': func.__module__, + 'closure_values': closure_values, + } + if hasattr(func, '__qualname__'): + state['qualname'] = func.__qualname__ + save(state) write(pickle.TUPLE) write(pickle.REDUCE) # applies _fill_function on the tuple @@ -944,27 +949,39 @@ def __reduce__(cls): def _fill_function(*args): - if len(args) == 5: + """Fills in the rest of function data into the skeleton function object + + The skeleton itself is create by _make_skel_func(). + """ + if len(args) == 2: + func = args[0] + state = args[1] + elif len(args) == 5: # Backwards compat for cloudpickle v0.4.0, after which the `module` - # argument was introduced - updated_args = args[:-1] + (None, args[-1],) - return _fill_function_internal(*updated_args) + # argument was introduced + func = args[0] + keys = ['globals', 'defaults', 'dict', 'closure_values'] + state = dict(zip(keys, args[1:])) + state['module'] = None + elif len(args) == 6: + # Backwards compat for cloudpickle v0.4.1, after which the function + # state was passed as a dict to the _fill_function it-self. + func = args[0] + keys = ['globals', 'defaults', 'dict', 'module', 'closure_values'] + state = dict(zip(keys, args[1:])) else: - return _fill_function_internal(*args) + raise ValueError('Unexpected _fill_value arguments: %r' % (args,)) - -def _fill_function_internal(func, globals, defaults, dict, module, closure_values): - """ Fills in the rest of function data into the skeleton function object - that were created via _make_skel_func(). - """ - func.__globals__.update(globals) - func.__defaults__ = defaults - func.__dict__ = dict - func.__module__ = module + func.__globals__.update(state['globals']) + func.__defaults__ = state['defaults'] + func.__dict__ = state['dict'] + func.__module__ = state['module'] + if 'qualname' in state: + func.__qualname__ = state['qualname'] cells = func.__closure__ if cells is not None: - for cell, value in zip(cells, closure_values): + for cell, value in zip(cells, state['closure_values']): if value is not _empty_cell_value: cell_set(cell, value) diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 898e11ea4..caa3e66d6 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -703,6 +703,18 @@ def test_function_module_name(self): func = lambda x: x self.assertEqual(pickle_depickle(func).__module__, func.__module__) + def test_function_qualname(self): + def func(x): + return x + # Default __qualname__ attribute (Python 3 only) + if hasattr(func, '__qualname__'): + self.assertEqual(pickle_depickle(func).__qualname__, + func.__qualname__) + + # Mutated __qualname__ attribute + func.__qualname__ = '' + self.assertEqual(pickle_depickle(func).__qualname__, func.__qualname__) + def test_namedtuple(self): MyTuple = collections.namedtuple('MyTuple', ['a', 'b', 'c']) From 11b0d33274cc7b6a4e937b8c2e17f358ec0162cf Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Wed, 8 Nov 2017 17:15:01 +0100 Subject: [PATCH 2/2] Do not set __module__ if not present in the pickled payload --- cloudpickle/cloudpickle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 4ce66ce75..45cffe5ba 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -962,7 +962,6 @@ def _fill_function(*args): func = args[0] keys = ['globals', 'defaults', 'dict', 'closure_values'] state = dict(zip(keys, args[1:])) - state['module'] = None elif len(args) == 6: # Backwards compat for cloudpickle v0.4.1, after which the function # state was passed as a dict to the _fill_function it-self. @@ -975,7 +974,8 @@ def _fill_function(*args): func.__globals__.update(state['globals']) func.__defaults__ = state['defaults'] func.__dict__ = state['dict'] - func.__module__ = state['module'] + if 'module' in state: + func.__module__ = state['module'] if 'qualname' in state: func.__qualname__ = state['qualname']