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

This function recomputes `cls.__abstractmethods__`.
Also update `@dataclass` to use it.
This commit is contained in:
Ben Avrahami 2020-10-06 20:40:50 +03:00 committed by GitHub
parent a8bf44d049
commit bef7d299eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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.