diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 030d44a3f..759cdb22a 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -492,8 +492,9 @@ def extract_func_data(self, func): # process closure closure = ( - [c.cell_contents for c in func.__closure__] - if func.__closure__ is not None else None + list(map(_get_cell_contents, func.__closure__)) + if func.__closure__ is not None + else None ) # save the dict @@ -896,6 +897,40 @@ def _gen_ellipsis(): def _gen_not_implemented(): return NotImplemented + +def _get_cell_contents(cell): + try: + return cell.cell_contents + except ValueError: + # sentinel used by ``_fill_function`` which will leave the cell empty + return _empty_cell_value + + +def instance(cls): + """Create a new instance of a class. + + Parameters + ---------- + cls : type + The class to create an instance of. + + Returns + ------- + instance : cls + A new instance of ``cls``. + """ + return cls() + + +@instance +class _empty_cell_value(object): + """sentinel for empty closures + """ + @classmethod + def __reduce__(cls): + return cls.__name__ + + def _fill_function(func, globals, defaults, dict, closure_values): """ Fills in the rest of function data into the skeleton function object that were created via _make_skel_func(). @@ -907,7 +942,8 @@ def _fill_function(func, globals, defaults, dict, closure_values): cells = func.__closure__ if cells is not None: for cell, value in zip(cells, closure_values): - cell_set(cell, value) + if value is not _empty_cell_value: + cell_set(cell, value) return func diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 19f1faf1f..f27cb7cba 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -167,6 +167,24 @@ def f(): msg='g now has closure cells even though f does not', ) + def test_empty_cell_preserved(self): + def f(): + if False: # pragma: no cover + cell = None + + def g(): + cell # NameError, unbound free variable + + return g + + g1 = f() + with pytest.raises(NameError): + g1() + + g2 = pickle_depickle(g1) + with pytest.raises(NameError): + g2() + def test_unhashable_closure(self): def f(): s = set((1, 2)) # mutable set is unhashable