diff --git a/CHANGES.md b/CHANGES.md index 89089865..972cf3f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,9 @@ 2.1.0 (in development) ====================== - +- Support for pickling `abc.abstractproperty`, `abc.abstractclassmethod`, + and `abc.abstractstaticmethod`. + ([PR #450](https://github.com/cloudpipe/cloudpickle/pull/450)) 2.0.0 ===== diff --git a/cloudpickle/cloudpickle_fast.py b/cloudpickle/cloudpickle_fast.py index ac4d08bb..70b5ecec 100644 --- a/cloudpickle/cloudpickle_fast.py +++ b/cloudpickle/cloudpickle_fast.py @@ -534,6 +534,10 @@ class CloudPickler(Pickler): _dispatch_table[type(OrderedDict().keys())] = _odict_keys_reduce _dispatch_table[type(OrderedDict().values())] = _odict_values_reduce _dispatch_table[type(OrderedDict().items())] = _odict_items_reduce + _dispatch_table[abc.abstractmethod] = _classmethod_reduce + _dispatch_table[abc.abstractclassmethod] = _classmethod_reduce + _dispatch_table[abc.abstractstaticmethod] = _classmethod_reduce + _dispatch_table[abc.abstractproperty] = _property_reduce dispatch_table = ChainMap(_dispatch_table, copyreg.dispatch_table) diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 22e31d8d..f356681d 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -1184,6 +1184,101 @@ def some_classmethod(cls): def some_staticmethod(): """A staticmethod""" + @property + @abc.abstractmethod + def some_property(): + """A property""" + + class ConcreteClass(AbstractClass): + def some_method(self): + return 'it works!' + + @classmethod + def some_classmethod(cls): + assert cls == ConcreteClass + return 'it works!' + + @staticmethod + def some_staticmethod(): + return 'it works!' + + @property + def some_property(self): + return 'it works!' + + # This abstract class is locally defined so we can safely register + # tuple in it to verify the unpickled class also register tuple. + AbstractClass.register(tuple) + + concrete_instance = ConcreteClass() + depickled_base = pickle_depickle(AbstractClass, protocol=self.protocol) + depickled_class = pickle_depickle(ConcreteClass, + protocol=self.protocol) + depickled_instance = pickle_depickle(concrete_instance) + + assert issubclass(tuple, AbstractClass) + assert issubclass(tuple, depickled_base) + + self.assertEqual(depickled_class().some_method(), 'it works!') + self.assertEqual(depickled_instance.some_method(), 'it works!') + + self.assertEqual(depickled_class.some_classmethod(), 'it works!') + self.assertEqual(depickled_instance.some_classmethod(), 'it works!') + + self.assertEqual(depickled_class().some_staticmethod(), 'it works!') + self.assertEqual(depickled_instance.some_staticmethod(), 'it works!') + + self.assertEqual(depickled_class().some_property, 'it works!') + self.assertEqual(depickled_instance.some_property, 'it works!') + self.assertRaises(TypeError, depickled_base) + + class DepickledBaseSubclass(depickled_base): + def some_method(self): + return 'it works for realz!' + + @classmethod + def some_classmethod(cls): + assert cls == DepickledBaseSubclass + return 'it works for realz!' + + @staticmethod + def some_staticmethod(): + return 'it works for realz!' + + @property + def some_property(): + return 'it works for realz!' + + self.assertEqual(DepickledBaseSubclass().some_method(), + 'it works for realz!') + + class IncompleteBaseSubclass(depickled_base): + def some_method(self): + return 'this class lacks some concrete methods' + + self.assertRaises(TypeError, IncompleteBaseSubclass) + + def test_abstracts(self): + # Same as `test_abc` but using deprecated `abc.abstract*` methods. + # See https://github.com/cloudpipe/cloudpickle/issues/367 + + class AbstractClass(abc.ABC): + @abc.abstractmethod + def some_method(self): + """A method""" + + @abc.abstractclassmethod + def some_classmethod(cls): + """A classmethod""" + + @abc.abstractstaticmethod + def some_staticmethod(): + """A staticmethod""" + + @abc.abstractproperty + def some_property(self): + """A property""" + class ConcreteClass(AbstractClass): def some_method(self): return 'it works!' @@ -1197,6 +1292,10 @@ def some_classmethod(cls): def some_staticmethod(): return 'it works!' + @property + def some_property(self): + return 'it works!' + # This abstract class is locally defined so we can safely register # tuple in it to verify the unpickled class also register tuple. AbstractClass.register(tuple) @@ -1218,6 +1317,9 @@ def some_staticmethod(): self.assertEqual(depickled_class().some_staticmethod(), 'it works!') self.assertEqual(depickled_instance.some_staticmethod(), 'it works!') + + self.assertEqual(depickled_class().some_property, 'it works!') + self.assertEqual(depickled_instance.some_property, 'it works!') self.assertRaises(TypeError, depickled_base) class DepickledBaseSubclass(depickled_base): @@ -1233,6 +1335,10 @@ def some_classmethod(cls): def some_staticmethod(): return 'it works for realz!' + @property + def some_property(self): + return 'it works for realz!' + self.assertEqual(DepickledBaseSubclass().some_method(), 'it works for realz!')