Skip to content

Commit 2d09ca1

Browse files
committed
FIX pass the __dict__ item of a class __dict__
1 parent 2db58f1 commit 2d09ca1

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
@@ -1867,6 +1867,29 @@ def __getattr__(self, name):
18671867
with pytest.raises(pickle.PicklingError, match='recursion'):
18681868
cloudpickle.dumps(a)
18691869

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

18711894
class Protocol2CloudPickleTest(CloudPickleTest):
18721895

0 commit comments

Comments
 (0)