Skip to content

Commit 10491eb

Browse files
Scott Sandersonrgbkrk
authored andcommitted
BUG: Support WeakSets and ABCMeta instances.
Fixes a bug where pickling instances of instances of ABCMeta (you read that correctly) would fail because ABCMeta uses several (previously unpicklable) WeakSets as caches. We fix the issue by adding support for pickling WeakSets via `save_reduce`.
1 parent aec80d2 commit 10491eb

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

cloudpickle/cloudpickle.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,13 @@ def save_not_implemented(self, obj):
858858
dispatch[type(Ellipsis)] = save_ellipsis
859859
dispatch[type(NotImplemented)] = save_not_implemented
860860

861+
# WeakSet was added in 2.7.
862+
if sys.version_info >= (2, 7):
863+
def save_weakset(self, obj):
864+
self.save_reduce(weakref.WeakSet, (list(obj),))
865+
866+
dispatch[weakref.WeakSet] = save_weakset
867+
861868
"""Special functions for Add-on libraries"""
862869
def inject_addons(self):
863870
"""Plug in system. Register additional pickling functions if modules already loaded"""

tests/cloudpickle_test.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import division
22

3+
import abc
4+
35
import base64
46
import functools
57
import imp
@@ -14,6 +16,7 @@
1416
import sys
1517
import textwrap
1618
import unittest
19+
import weakref
1720

1821
try:
1922
from StringIO import StringIO
@@ -43,6 +46,9 @@
4346
from .testutils import subprocess_pickle_echo
4447

4548

49+
HAVE_WEAKSET = sys.version_info >= (2, 7)
50+
51+
4652
def pickle_depickle(obj):
4753
"""Helper function to test whether object pickled with cloudpickle can be
4854
depickled with pickle
@@ -592,6 +598,62 @@ def test_logger(self):
592598
self.assertEqual(out.strip().decode(),
593599
'INFO:cloudpickle.dummy_test_logger:hello')
594600

601+
def test_abc(self):
602+
603+
@abc.abstractmethod
604+
def foo(self):
605+
raise NotImplementedError('foo')
606+
607+
# Invoke the metaclass directly rather than using class syntax for
608+
# python 2/3 compat.
609+
AbstractClass = abc.ABCMeta('AbstractClass', (object,), {'foo': foo})
610+
611+
class ConcreteClass(AbstractClass):
612+
def foo(self):
613+
return 'it works!'
614+
615+
depickled_base = pickle_depickle(AbstractClass)
616+
depickled_class = pickle_depickle(ConcreteClass)
617+
depickled_instance = pickle_depickle(ConcreteClass())
618+
619+
self.assertEqual(depickled_class().foo(), 'it works!')
620+
self.assertEqual(depickled_instance.foo(), 'it works!')
621+
622+
# It should still be invalid to construct an instance of the abstract
623+
# class without implementing its methods.
624+
with self.assertRaises(TypeError):
625+
depickled_base()
626+
627+
class DepickledBaseSubclass(depickled_base):
628+
def foo(self):
629+
return 'it works for realz!'
630+
631+
self.assertEqual(DepickledBaseSubclass().foo(), 'it works for realz!')
632+
633+
@pytest.mark.skipif(not HAVE_WEAKSET, reason="WeakSet doesn't exist")
634+
def test_weakset_identity_preservation(self):
635+
# Test that weaksets don't lose all their inhabitants if they're
636+
# pickled in a larger data structure that includes other references to
637+
# their inhabitants.
638+
639+
class SomeClass(object):
640+
def __init__(self, x):
641+
self.x = x
642+
643+
obj1, obj2, obj3 = SomeClass(1), SomeClass(2), SomeClass(3)
644+
645+
things = [weakref.WeakSet([obj1, obj2]), obj1, obj2, obj3]
646+
result = pickle_depickle(things)
647+
648+
weakset, depickled1, depickled2, depickled3 = result
649+
650+
self.assertEqual(depickled1.x, 1)
651+
self.assertEqual(depickled2.x, 2)
652+
self.assertEqual(depickled3.x, 3)
653+
self.assertEqual(len(weakset), 2)
654+
655+
self.assertEqual(set(weakset), set([depickled1, depickled2]))
656+
595657

596658
if __name__ == '__main__':
597659
unittest.main()

0 commit comments

Comments
 (0)