diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 1d9861d21..45cffe5ba 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:])) + 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'] + if 'module' in state: + 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'])