Skip to content

Commit d967827

Browse files
committed
FIX pass the __dict__ item of a class __dict__
1 parent d884a01 commit d967827

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

cloudpickle/cloudpickle.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -642,10 +642,23 @@ def save_dynamic_class(self, obj):
642642
for k in obj.__slots__:
643643
clsdict.pop(k, None)
644644

645-
# If type overrides __dict__ as a property, include it in the type
646-
# kwargs. In Python 2, we can't set this attribute after construction.
645+
# A class __dict__ is part of the class state. At unpickling time, it
646+
# must be *initialized* (in an empty state) during class creation and
647+
# updated during class re-hydratation.
648+
# However, a class __dict__ is read-only, and does not support direct
649+
# item assignement. Instead, way to update a class __dict__ is to call
650+
# setattr(k, v) on the underlying class, which has the same effect.
651+
# There is one corner case: if the __dict__ class has itself a
652+
# "__dict__" key (this means that the class likely overrides the
653+
# __dict__ property of its instances), setattr("__dict__", v) will try
654+
# to modify the read-only class __dict__ instead, and fail. As a
655+
# result, if it exists, the class __dict__ must contain its __dict__
656+
# item when it is initialized and fed to the class reconstructor.
647657
__dict__ = clsdict.pop('__dict__', None)
648-
if isinstance(__dict__, property):
658+
if __dict__ is not None:
659+
# Native pickle memoization of dict objects prevents us from
660+
# reference cycles even if __dict__ is now saved before obj is
661+
# memoized.
649662
type_kwargs['__dict__'] = __dict__
650663

651664
save = self.save

tests/cloudpickle_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,29 @@ def __getattr__(self, name):
18621862
with pytest.raises(pickle.PicklingError, match='recursion'):
18631863
cloudpickle.dumps(a)
18641864

1865+
def test___dict__attribute_not_dropped_during_pickling(self):
1866+
# Test https://github.com/cloudpipe/cloudpickle/issues/282. cloudpickle
1867+
# used to drop __dict__ attributes of classes at pickling time.
1868+
pickle_filename = os.path.join(self.tmpdir, 'class_with___dict__.pkl')
1869+
_dict = {'some_attribute': 1}
1870+
class A:
1871+
__dict__ = _dict
1872+
a = A()
1873+
self.assertEqual(a.__dict__, _dict)
1874+
1875+
with open(pickle_filename, "wb") as f:
1876+
cloudpickle.dump(a, f, protocol=self.protocol)
1877+
1878+
# Depickle the class in a new python session to make sure the class is
1879+
# fully-recreated, and not looked-up in existing cloudpickle
1880+
# class-tracking constructs.
1881+
child_process_script = """
1882+
import pickle
1883+
with open("{filename}", "rb") as f:
1884+
depickled_a = pickle.load(f)
1885+
assert depickled_a.__dict__ == {_dict}, depickled_a.__dict__
1886+
""".format(filename=pickle_filename, _dict=_dict)
1887+
assert_run_python_script(textwrap.dedent(child_process_script))
18651888

18661889
class Protocol2CloudPickleTest(CloudPickleTest):
18671890

0 commit comments

Comments
 (0)