[Python-checkins] bpo-41905: Add abc.update_abstractmethods() (GH-22485)

Ben Avrahami webhook-mailer at python.org
Tue Oct 6 13:41:19 EDT 2020


https://github.com/python/cpython/commit/bef7d299eb911086ea5a7ccf7a9da337e38a8491
commit: bef7d299eb911086ea5a7ccf7a9da337e38a8491
branch: master
author: Ben Avrahami <avrahami.ben at gmail.com>
committer: GitHub <noreply at github.com>
date: 2020-10-06T10:40:50-07:00
summary:

bpo-41905: Add abc.update_abstractmethods() (GH-22485)

This function recomputes `cls.__abstractmethods__`.
Also update `@dataclass` to use it.

files:
A Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst
M Doc/library/abc.rst
M Doc/library/functools.rst
M Lib/abc.py
M Lib/dataclasses.py
M Lib/test/test_abc.py
M Lib/test/test_dataclasses.py

diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst
index 424ae547d829a..3a7414d7358e7 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 14aa184e2cd14..186cb4c381dee 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 431b64040a66e..276ef9a2cd485 100644
--- a/Lib/abc.py
+++ b/Lib/abc.py
@@ -122,6 +122,44 @@ def _abc_caches_clear(cls):
             _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 530d3e99574e8..65091021f3716 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -6,6 +6,7 @@
 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 7e9c47b3cacb9..3d603e7734d87 100644
--- a/Lib/test/test_abc.py
+++ b/Lib/test/test_abc.py
@@ -488,6 +488,155 @@ class C(with_metaclass(abc_ABCMeta, A, B)):
                 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 b20103bdce51c..b31a469ec7922 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 C:
 
     ##     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 0000000000000..0d8c0ba6a66bd
--- /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



More information about the Python-checkins mailing list