diff --git a/Lib/abc.py b/Lib/abc.py index 431b64040..2ba3ee464 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -28,7 +28,14 @@ def abstractmethod(funcobj): class abstractclassmethod(classmethod): """A decorator indicating abstract classmethods. - Deprecated, use 'classmethod' with 'abstractmethod' instead. + Deprecated, use 'classmethod' with 'abstractmethod' instead: + + class C(ABC): + @classmethod + @abstractmethod + def my_abstract_classmethod(cls, ...): + ... + """ __isabstractmethod__ = True @@ -41,7 +48,14 @@ class abstractclassmethod(classmethod): class abstractstaticmethod(staticmethod): """A decorator indicating abstract staticmethods. - Deprecated, use 'staticmethod' with 'abstractmethod' instead. + Deprecated, use 'staticmethod' with 'abstractmethod' instead: + + class C(ABC): + @staticmethod + @abstractmethod + def my_abstract_staticmethod(...): + ... + """ __isabstractmethod__ = True @@ -54,7 +68,14 @@ class abstractstaticmethod(staticmethod): class abstractproperty(property): """A decorator indicating abstract properties. - Deprecated, use 'property' with 'abstractmethod' instead. + Deprecated, use 'property' with 'abstractmethod' instead: + + class C(ABC): + @property + @abstractmethod + def my_abstract_property(self): + ... + """ __isabstractmethod__ = True @@ -64,6 +85,10 @@ try: from _abc import (get_cache_token, _abc_init, _abc_register, _abc_instancecheck, _abc_subclasscheck, _get_dump, _reset_registry, _reset_caches) +# TODO: RUSTPYTHON missing _abc module implementation. +except ModuleNotFoundError: + from _py_abc import ABCMeta, get_cache_token + ABCMeta.__module__ = 'abc' except ImportError: from _py_abc import ABCMeta, get_cache_token ABCMeta.__module__ = 'abc' @@ -122,6 +147,44 @@ else: _reset_caches(cls) +def update_abstractmethods(cls): + """Recalculate the set of abstract methods of an abstract class. + + If a class has had one of its abstract methods implemented after the + class was created, the method will not be considered implemented until + this function is called. Alternatively, if a new abstract method has been + added to the class, it will only be considered an abstract method of the + class after this function is called. + + This function should be called before any use is made of the class, + usually in class decorators that add methods to the subject class. + + Returns cls, to allow usage as a class decorator. + + If cls is not an instance of ABCMeta, does nothing. + """ + if not hasattr(cls, '__abstractmethods__'): + # We check for __abstractmethods__ here because cls might by a C + # implementation or a python implementation (especially during + # testing), and we want to handle both cases. + return cls + + abstracts = set() + # Check the existing abstract methods of the parents, keep only the ones + # that are not implemented. + for scls in cls.__bases__: + for name in getattr(scls, '__abstractmethods__', ()): + value = getattr(cls, name, None) + if getattr(value, "__isabstractmethod__", False): + abstracts.add(name) + # Also add any other newly added abstract methods. + for name, value in cls.__dict__.items(): + if getattr(value, "__isabstractmethod__", False): + abstracts.add(name) + cls.__abstractmethods__ = frozenset(abstracts) + return cls + + class ABC(metaclass=ABCMeta): """Helper class that provides a standard way to create an ABC using inheritance. diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 5b897ba8c..993bb0620 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -157,8 +157,25 @@ def test_factory(abc_ABCMeta, abc_get_cache_token): self.assertEqual(D.foo(), 4) self.assertEqual(D().foo(), 4) - # TODO: RUSTPYTHON - @unittest.expectedFailure + def test_object_new_with_one_abstractmethod(self): + class C(metaclass=abc_ABCMeta): + @abc.abstractmethod + def method_one(self): + pass + msg = r"class C with abstract method method_one" + self.assertRaisesRegex(TypeError, msg, C) + + def test_object_new_with_many_abstractmethods(self): + class C(metaclass=abc_ABCMeta): + @abc.abstractmethod + def method_one(self): + pass + @abc.abstractmethod + def method_two(self): + pass + msg = r"class C with abstract methods method_one, method_two" + self.assertRaisesRegex(TypeError, msg, C) + def test_abstractmethod_integration(self): for abstractthing in [abc.abstractmethod, abc.abstractproperty, abc.abstractclassmethod, @@ -219,6 +236,7 @@ def test_factory(abc_ABCMeta, abc_get_cache_token): bar.__isabstractmethod__ = NotBool() foo = property(bar) + def test_customdescriptors_with_abstractmethod(self): class Descriptor: def __init__(self, fget, fset=None): @@ -318,7 +336,7 @@ def test_factory(abc_ABCMeta, abc_get_cache_token): token_old = abc_get_cache_token() A.register(B) token_new = abc_get_cache_token() - self.assertNotEqual(token_old, token_new) + self.assertGreater(token_new, token_old) self.assertTrue(isinstance(b, A)) self.assertTrue(isinstance(b, (A,))) @@ -451,6 +469,24 @@ def test_factory(abc_ABCMeta, abc_get_cache_token): with self.assertRaisesRegex(Exception, exc_msg): issubclass(int, S) + def test_subclasshook(self): + class A(metaclass=abc.ABCMeta): + @classmethod + def __subclasshook__(cls, C): + if cls is A: + return 'foo' in C.__dict__ + return NotImplemented + self.assertFalse(issubclass(A, A)) + self.assertFalse(issubclass(A, (A,))) + class B: + foo = 42 + self.assertTrue(issubclass(B, A)) + self.assertTrue(issubclass(B, (A,))) + class C: + spam = 42 + self.assertFalse(issubclass(C, A)) + self.assertFalse(issubclass(C, (A,))) + def test_all_new_methods_are_called(self): class A(metaclass=abc_ABCMeta): pass @@ -480,6 +516,155 @@ def test_factory(abc_ABCMeta, abc_get_cache_token): pass self.assertEqual(C.__class__, abc_ABCMeta) + def test_update_del(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + del A.foo + self.assertEqual(A.__abstractmethods__, {'foo'}) + self.assertFalse(hasattr(A, 'foo')) + + abc.update_abstractmethods(A) + + self.assertEqual(A.__abstractmethods__, set()) + A() + + + def test_update_new_abstractmethods(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def bar(self): + pass + + @abc.abstractmethod + def updated_foo(self): + pass + + A.foo = updated_foo + abc.update_abstractmethods(A) + self.assertEqual(A.__abstractmethods__, {'foo', 'bar'}) + msg = "class A with abstract methods bar, foo" + self.assertRaisesRegex(TypeError, msg, A) + + def test_update_implementation(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + class B(A): + pass + + msg = "class B with abstract method foo" + self.assertRaisesRegex(TypeError, msg, B) + self.assertEqual(B.__abstractmethods__, {'foo'}) + + B.foo = lambda self: None + + abc.update_abstractmethods(B) + + B() + self.assertEqual(B.__abstractmethods__, set()) + + def test_update_as_decorator(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + def class_decorator(cls): + cls.foo = lambda self: None + return cls + + @abc.update_abstractmethods + @class_decorator + class B(A): + pass + + B() + self.assertEqual(B.__abstractmethods__, set()) + + def test_update_non_abc(self): + class A: + pass + + @abc.abstractmethod + def updated_foo(self): + pass + + A.foo = updated_foo + abc.update_abstractmethods(A) + A() + self.assertFalse(hasattr(A, '__abstractmethods__')) + + def test_update_del_implementation(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + class B(A): + def foo(self): + pass + + B() + + del B.foo + + abc.update_abstractmethods(B) + + msg = "class B with abstract method foo" + self.assertRaisesRegex(TypeError, msg, B) + + def test_update_layered_implementation(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + class B(A): + pass + + class C(B): + def foo(self): + pass + + C() + + del C.foo + + abc.update_abstractmethods(C) + + msg = "class C with abstract method foo" + self.assertRaisesRegex(TypeError, msg, C) + + def test_update_multi_inheritance(self): + class A(metaclass=abc_ABCMeta): + @abc.abstractmethod + def foo(self): + pass + + class B(metaclass=abc_ABCMeta): + def foo(self): + pass + + class C(B, A): + @abc.abstractmethod + def foo(self): + pass + + self.assertEqual(C.__abstractmethods__, {'foo'}) + + del C.foo + + abc.update_abstractmethods(C) + + self.assertEqual(C.__abstractmethods__, set()) + + C() + class TestABCWithInitSubclass(unittest.TestCase): def test_works_with_init_subclass(self):