bpo-41905: Add abc.update_abstractmethods() (GH-22485)
This function recomputes `cls.__abstractmethods__`. Also update `@dataclass` to use it.
This commit is contained in:
parent
a8bf44d049
commit
bef7d299eb
|
@ -174,10 +174,11 @@ The :mod:`abc` module also provides the following decorator:
|
||||||
to declare abstract methods for properties and descriptors.
|
to declare abstract methods for properties and descriptors.
|
||||||
|
|
||||||
Dynamically adding abstract methods to a class, or attempting to modify the
|
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
|
abstraction status of a method or class once it is created, are only
|
||||||
supported. The :func:`abstractmethod` only affects subclasses derived using
|
supported using the :func:`update_abstractmethods` function. The
|
||||||
regular inheritance; "virtual subclasses" registered with the ABC's
|
:func:`abstractmethod` only affects subclasses derived using regular
|
||||||
:meth:`register` method are not affected.
|
inheritance; "virtual subclasses" registered with the ABC's :meth:`register`
|
||||||
|
method are not affected.
|
||||||
|
|
||||||
When :func:`abstractmethod` is applied in combination with other method
|
When :func:`abstractmethod` is applied in combination with other method
|
||||||
descriptors, it should be applied as the innermost decorator, as shown in
|
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
|
super-call in a framework that uses cooperative
|
||||||
multiple-inheritance.
|
multiple-inheritance.
|
||||||
|
|
||||||
|
|
||||||
The :mod:`abc` module also supports the following legacy decorators:
|
The :mod:`abc` module also supports the following legacy decorators:
|
||||||
|
|
||||||
.. decorator:: abstractclassmethod
|
.. decorator:: abstractclassmethod
|
||||||
|
@ -335,6 +335,22 @@ The :mod:`abc` module also provides the following functions:
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
.. 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
|
.. rubric:: Footnotes
|
||||||
|
|
||||||
|
|
|
@ -254,6 +254,13 @@ The :mod:`functools` module defines the following functions:
|
||||||
application, implementing all six rich comparison methods instead is
|
application, implementing all six rich comparison methods instead is
|
||||||
likely to provide an easy speed boost.
|
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
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
.. versionchanged:: 3.4
|
.. versionchanged:: 3.4
|
||||||
|
|
38
Lib/abc.py
38
Lib/abc.py
|
@ -122,6 +122,44 @@ else:
|
||||||
_reset_caches(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):
|
class ABC(metaclass=ABCMeta):
|
||||||
"""Helper class that provides a standard way to create an ABC using
|
"""Helper class that provides a standard way to create an ABC using
|
||||||
inheritance.
|
inheritance.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import inspect
|
||||||
import keyword
|
import keyword
|
||||||
import builtins
|
import builtins
|
||||||
import functools
|
import functools
|
||||||
|
import abc
|
||||||
import _thread
|
import _thread
|
||||||
from types import GenericAlias
|
from types import GenericAlias
|
||||||
|
|
||||||
|
@ -992,6 +993,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||||
cls.__doc__ = (cls.__name__ +
|
cls.__doc__ = (cls.__name__ +
|
||||||
str(inspect.signature(cls)).replace(' -> None', ''))
|
str(inspect.signature(cls)).replace(' -> None', ''))
|
||||||
|
|
||||||
|
abc.update_abstractmethods(cls)
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -488,6 +488,155 @@ def test_factory(abc_ABCMeta, abc_get_cache_token):
|
||||||
pass
|
pass
|
||||||
self.assertEqual(C.__class__, abc_ABCMeta)
|
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):
|
class TestABCWithInitSubclass(unittest.TestCase):
|
||||||
def test_works_with_init_subclass(self):
|
def test_works_with_init_subclass(self):
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
from dataclasses import *
|
from dataclasses import *
|
||||||
|
|
||||||
|
import abc
|
||||||
import pickle
|
import pickle
|
||||||
import inspect
|
import inspect
|
||||||
import builtins
|
import builtins
|
||||||
|
@ -3332,6 +3333,42 @@ class TestReplace(unittest.TestCase):
|
||||||
|
|
||||||
## replace(c, x=5)
|
## 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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue