summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/abc.rst26
-rw-r--r--Doc/library/functools.rst7
-rw-r--r--Lib/abc.py38
-rw-r--r--Lib/dataclasses.py3
-rw-r--r--Lib/test/test_abc.py149
-rw-r--r--Lib/test/test_dataclasses.py37
-rw-r--r--Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst1
7 files changed, 256 insertions, 5 deletions
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