From bef7d299eb911086ea5a7ccf7a9da337e38a8491 Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Tue, 6 Oct 2020 20:40:50 +0300 Subject: bpo-41905: Add abc.update_abstractmethods() (GH-22485) This function recomputes `cls.__abstractmethods__`. Also update `@dataclass` to use it. --- Doc/library/abc.rst | 26 +++- Doc/library/functools.rst | 7 + Lib/abc.py | 38 ++++++ Lib/dataclasses.py | 3 + Lib/test/test_abc.py | 149 +++++++++++++++++++++ Lib/test/test_dataclasses.py | 37 +++++ .../2020-10-01-21-11-03.bpo-41905._JpjR4.rst | 1 + 7 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 424ae54..3a7414d 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -174,10 +174,11 @@ The :mod:`abc` module also provides the following decorator: to declare abstract methods for properties and descriptors. Dynamically adding abstract methods to a class, or attempting to modify the - abstraction status of a method or class once it is created, are not - supported. The :func:`abstractmethod` only affects subclasses derived using - regular inheritance; "virtual subclasses" registered with the ABC's - :meth:`register` method are not affected. + abstraction status of a method or class once it is created, are only + supported using the :func:`update_abstractmethods` function. The + :func:`abstractmethod` only affects subclasses derived using regular + inheritance; "virtual subclasses" registered with the ABC's :meth:`register` + method are not affected. When :func:`abstractmethod` is applied in combination with other method descriptors, it should be applied as the innermost decorator, as shown in @@ -235,7 +236,6 @@ The :mod:`abc` module also provides the following decorator: super-call in a framework that uses cooperative multiple-inheritance. - The :mod:`abc` module also supports the following legacy decorators: .. decorator:: abstractclassmethod @@ -335,6 +335,22 @@ The :mod:`abc` module also provides the following functions: .. versionadded:: 3.4 +.. function:: update_abstractmethods(cls) + A function to recalculate an abstract class's abstraction status. This + function should be called if a class's abstract methods have been + implemented or changed after it was created. Usually, this function should + be called from within a class decorator. + + Returns *cls*, to allow usage as a class decorator. + + If *cls* is not an instance of ABCMeta, does nothing. + + .. note:: + + This function assumes that *cls*'s superclasses are already updated. + It does not update any subclasses. + + .. versionadded:: 3.10 .. rubric:: Footnotes diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 14aa184..186cb4c 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -254,6 +254,13 @@ The :mod:`functools` module defines the following functions: application, implementing all six rich comparison methods instead is likely to provide an easy speed boost. + .. note:: + + This decorator makes no attempt to override methods that have been + declared in the class *or its superclasses*. Meaning that if a + superclass defines a comparison operator, *total_ordering* will not + implement it again, even if the original method is abstract. + .. versionadded:: 3.2 .. versionchanged:: 3.4 diff --git a/Lib/abc.py b/Lib/abc.py index 431b640..276ef9a 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -122,6 +122,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/dataclasses.py b/Lib/dataclasses.py index 530d3e9..6509102 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -6,6 +6,7 @@ import inspect import keyword import builtins import functools +import abc import _thread from types import GenericAlias @@ -992,6 +993,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): cls.__doc__ = (cls.__name__ + str(inspect.signature(cls)).replace(' -> None', '')) + abc.update_abstractmethods(cls) + return cls diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 7e9c47b..3d603e7 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -488,6 +488,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): diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index b20103b..b31a469 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -4,6 +4,7 @@ from dataclasses import * +import abc import pickle import inspect import builtins @@ -3332,6 +3333,42 @@ class TestReplace(unittest.TestCase): ## replace(c, x=5) +class TestAbstract(unittest.TestCase): + def test_abc_implementation(self): + class Ordered(abc.ABC): + @abc.abstractmethod + def __lt__(self, other): + pass + + @abc.abstractmethod + def __le__(self, other): + pass + + @dataclass(order=True) + class Date(Ordered): + year: int + month: 'Month' + day: 'int' + + self.assertFalse(inspect.isabstract(Date)) + self.assertGreater(Date(2020,12,25), Date(2020,8,31)) + + def test_maintain_abc(self): + class A(abc.ABC): + @abc.abstractmethod + def foo(self): + pass + + @dataclass + class Date(A): + year: int + month: 'Month' + day: 'int' + + self.assertTrue(inspect.isabstract(Date)) + msg = 'class Date with abstract method foo' + self.assertRaisesRegex(TypeError, msg, Date) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst b/Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst new file mode 100644 index 0000000..0d8c0ba --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst @@ -0,0 +1 @@ +A new function in abc: *update_abstractmethods* to re-calculate an abstract class's abstract status. In addition, *dataclass* has been changed to call this function. \ No newline at end of file -- cgit v0.12