From d9678275ed3beb3fef7b0b8b2f4d38bed183d7c1 Mon Sep 17 00:00:00 2001 From: Pierre Glaser Date: Mon, 10 Jun 2019 16:44:44 +0200 Subject: [PATCH] FIX pass the __dict__ item of a class __dict__ --- cloudpickle/cloudpickle.py | 19 ++++++++++++++++--- tests/cloudpickle_test.py | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index ad0f5bb58..888d52848 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -642,10 +642,23 @@ def save_dynamic_class(self, obj): for k in obj.__slots__: clsdict.pop(k, None) - # If type overrides __dict__ as a property, include it in the type - # kwargs. In Python 2, we can't set this attribute after construction. + # A class __dict__ is part of the class state. At unpickling time, it + # must be *initialized* (in an empty state) during class creation and + # updated during class re-hydratation. + # However, a class __dict__ is read-only, and does not support direct + # item assignement. Instead, way to update a class __dict__ is to call + # setattr(k, v) on the underlying class, which has the same effect. + # There is one corner case: if the __dict__ class has itself a + # "__dict__" key (this means that the class likely overrides the + # __dict__ property of its instances), setattr("__dict__", v) will try + # to modify the read-only class __dict__ instead, and fail. As a + # result, if it exists, the class __dict__ must contain its __dict__ + # item when it is initialized and fed to the class reconstructor. __dict__ = clsdict.pop('__dict__', None) - if isinstance(__dict__, property): + if __dict__ is not None: + # Native pickle memoization of dict objects prevents us from + # reference cycles even if __dict__ is now saved before obj is + # memoized. type_kwargs['__dict__'] = __dict__ save = self.save diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 75287f6e6..5d5b8905b 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -1862,6 +1862,29 @@ def __getattr__(self, name): with pytest.raises(pickle.PicklingError, match='recursion'): cloudpickle.dumps(a) + def test___dict__attribute_not_dropped_during_pickling(self): + # Test https://github.com/cloudpipe/cloudpickle/issues/282. cloudpickle + # used to drop __dict__ attributes of classes at pickling time. + pickle_filename = os.path.join(self.tmpdir, 'class_with___dict__.pkl') + _dict = {'some_attribute': 1} + class A: + __dict__ = _dict + a = A() + self.assertEqual(a.__dict__, _dict) + + with open(pickle_filename, "wb") as f: + cloudpickle.dump(a, f, protocol=self.protocol) + + # Depickle the class in a new python session to make sure the class is + # fully-recreated, and not looked-up in existing cloudpickle + # class-tracking constructs. + child_process_script = """ + import pickle + with open("{filename}", "rb") as f: + depickled_a = pickle.load(f) + assert depickled_a.__dict__ == {_dict}, depickled_a.__dict__ + """.format(filename=pickle_filename, _dict=_dict) + assert_run_python_script(textwrap.dedent(child_process_script)) class Protocol2CloudPickleTest(CloudPickleTest):