Skip to content

Commit bef7d29

Browse files
authored
bpo-41905: Add abc.update_abstractmethods() (GH-22485)
This function recomputes `cls.__abstractmethods__`. Also update `@dataclass` to use it.
1 parent a8bf44d commit bef7d29

File tree

7 files changed

+256
-5
lines changed

7 files changed

+256
-5
lines changed

Doc/library/abc.rst

+21-5
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,11 @@ The :mod:`abc` module also provides the following decorator:
174174
to declare abstract methods for properties and descriptors.
175175

176176
Dynamically adding abstract methods to a class, or attempting to modify the
177-
abstraction status of a method or class once it is created, are not
178-
supported. The :func:`abstractmethod` only affects subclasses derived using
179-
regular inheritance; "virtual subclasses" registered with the ABC's
180-
:meth:`register` method are not affected.
177+
abstraction status of a method or class once it is created, are only
178+
supported using the :func:`update_abstractmethods` function. The
179+
:func:`abstractmethod` only affects subclasses derived using regular
180+
inheritance; "virtual subclasses" registered with the ABC's :meth:`register`
181+
method are not affected.
181182

182183
When :func:`abstractmethod` is applied in combination with other method
183184
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:
235236
super-call in a framework that uses cooperative
236237
multiple-inheritance.
237238

238-
239239
The :mod:`abc` module also supports the following legacy decorators:
240240

241241
.. decorator:: abstractclassmethod
@@ -335,6 +335,22 @@ The :mod:`abc` module also provides the following functions:
335335

336336
.. versionadded:: 3.4
337337

338+
.. function:: update_abstractmethods(cls)
339+
A function to recalculate an abstract class's abstraction status. This
340+
function should be called if a class's abstract methods have been
341+
implemented or changed after it was created. Usually, this function should
342+
be called from within a class decorator.
343+
344+
Returns *cls*, to allow usage as a class decorator.
345+
346+
If *cls* is not an instance of ABCMeta, does nothing.
347+
348+
.. note::
349+
350+
This function assumes that *cls*'s superclasses are already updated.
351+
It does not update any subclasses.
352+
353+
.. versionadded:: 3.10
338354

339355
.. rubric:: Footnotes
340356

Doc/library/functools.rst

+7
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,13 @@ The :mod:`functools` module defines the following functions:
254254
application, implementing all six rich comparison methods instead is
255255
likely to provide an easy speed boost.
256256

257+
.. note::
258+
259+
This decorator makes no attempt to override methods that have been
260+
declared in the class *or its superclasses*. Meaning that if a
261+
superclass defines a comparison operator, *total_ordering* will not
262+
implement it again, even if the original method is abstract.
263+
257264
.. versionadded:: 3.2
258265

259266
.. versionchanged:: 3.4

Lib/abc.py

+38
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,44 @@ def _abc_caches_clear(cls):
122122
_reset_caches(cls)
123123

124124

125+
def update_abstractmethods(cls):
126+
"""Recalculate the set of abstract methods of an abstract class.
127+
128+
If a class has had one of its abstract methods implemented after the
129+
class was created, the method will not be considered implemented until
130+
this function is called. Alternatively, if a new abstract method has been
131+
added to the class, it will only be considered an abstract method of the
132+
class after this function is called.
133+
134+
This function should be called before any use is made of the class,
135+
usually in class decorators that add methods to the subject class.
136+
137+
Returns cls, to allow usage as a class decorator.
138+
139+
If cls is not an instance of ABCMeta, does nothing.
140+
"""
141+
if not hasattr(cls, '__abstractmethods__'):
142+
# We check for __abstractmethods__ here because cls might by a C
143+
# implementation or a python implementation (especially during
144+
# testing), and we want to handle both cases.
145+
return cls
146+
147+
abstracts = set()
148+
# Check the existing abstract methods of the parents, keep only the ones
149+
# that are not implemented.
150+
for scls in cls.__bases__:
151+
for name in getattr(scls, '__abstractmethods__', ()):
152+
value = getattr(cls, name, None)
153+
if getattr(value, "__isabstractmethod__", False):
154+
abstracts.add(name)
155+
# Also add any other newly added abstract methods.
156+
for name, value in cls.__dict__.items():
157+
if getattr(value, "__isabstractmethod__", False):
158+
abstracts.add(name)
159+
cls.__abstractmethods__ = frozenset(abstracts)
160+
return cls
161+
162+
125163
class ABC(metaclass=ABCMeta):
126164
"""Helper class that provides a standard way to create an ABC using
127165
inheritance.

Lib/dataclasses.py

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import keyword
77
import builtins
88
import functools
9+
import abc
910
import _thread
1011
from types import GenericAlias
1112

@@ -992,6 +993,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
992993
cls.__doc__ = (cls.__name__ +
993994
str(inspect.signature(cls)).replace(' -> None', ''))
994995

996+
abc.update_abstractmethods(cls)
997+
995998
return cls
996999

9971000

Lib/test/test_abc.py

+149
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,155 @@ class C(with_metaclass(abc_ABCMeta, A, B)):
488488
pass
489489
self.assertEqual(C.__class__, abc_ABCMeta)
490490

491+
def test_update_del(self):
492+
class A(metaclass=abc_ABCMeta):
493+
@abc.abstractmethod
494+
def foo(self):
495+
pass
496+
497+
del A.foo
498+
self.assertEqual(A.__abstractmethods__, {'foo'})
499+
self.assertFalse(hasattr(A, 'foo'))
500+
501+
abc.update_abstractmethods(A)
502+
503+
self.assertEqual(A.__abstractmethods__, set())
504+
A()
505+
506+
507+
def test_update_new_abstractmethods(self):
508+
class A(metaclass=abc_ABCMeta):
509+
@abc.abstractmethod
510+
def bar(self):
511+
pass
512+
513+
@abc.abstractmethod
514+
def updated_foo(self):
515+
pass
516+
517+
A.foo = updated_foo
518+
abc.update_abstractmethods(A)
519+
self.assertEqual(A.__abstractmethods__, {'foo', 'bar'})
520+
msg = "class A with abstract methods bar, foo"
521+
self.assertRaisesRegex(TypeError, msg, A)
522+
523+
def test_update_implementation(self):
524+
class A(metaclass=abc_ABCMeta):
525+
@abc.abstractmethod
526+
def foo(self):
527+
pass
528+
529+
class B(A):
530+
pass
531+
532+
msg = "class B with abstract method foo"
533+
self.assertRaisesRegex(TypeError, msg, B)
534+
self.assertEqual(B.__abstractmethods__, {'foo'})
535+
536+
B.foo = lambda self: None
537+
538+
abc.update_abstractmethods(B)
539+
540+
B()
541+
self.assertEqual(B.__abstractmethods__, set())
542+
543+
def test_update_as_decorator(self):
544+
class A(metaclass=abc_ABCMeta):
545+
@abc.abstractmethod
546+
def foo(self):
547+
pass
548+
549+
def class_decorator(cls):
550+
cls.foo = lambda self: None
551+
return cls
552+
553+
@abc.update_abstractmethods
554+
@class_decorator
555+
class B(A):
556+
pass
557+
558+
B()
559+
self.assertEqual(B.__abstractmethods__, set())
560+
561+
def test_update_non_abc(self):
562+
class A:
563+
pass
564+
565+
@abc.abstractmethod
566+
def updated_foo(self):
567+
pass
568+
569+
A.foo = updated_foo
570+
abc.update_abstractmethods(A)
571+
A()
572+
self.assertFalse(hasattr(A, '__abstractmethods__'))
573+
574+
def test_update_del_implementation(self):
575+
class A(metaclass=abc_ABCMeta):
576+
@abc.abstractmethod
577+
def foo(self):
578+
pass
579+
580+
class B(A):
581+
def foo(self):
582+
pass
583+
584+
B()
585+
586+
del B.foo
587+
588+
abc.update_abstractmethods(B)
589+
590+
msg = "class B with abstract method foo"
591+
self.assertRaisesRegex(TypeError, msg, B)
592+
593+
def test_update_layered_implementation(self):
594+
class A(metaclass=abc_ABCMeta):
595+
@abc.abstractmethod
596+
def foo(self):
597+
pass
598+
599+
class B(A):
600+
pass
601+
602+
class C(B):
603+
def foo(self):
604+
pass
605+
606+
C()
607+
608+
del C.foo
609+
610+
abc.update_abstractmethods(C)
611+
612+
msg = "class C with abstract method foo"
613+
self.assertRaisesRegex(TypeError, msg, C)
614+
615+
def test_update_multi_inheritance(self):
616+
class A(metaclass=abc_ABCMeta):
617+
@abc.abstractmethod
618+
def foo(self):
619+
pass
620+
621+
class B(metaclass=abc_ABCMeta):
622+
def foo(self):
623+
pass
624+
625+
class C(B, A):
626+
@abc.abstractmethod
627+
def foo(self):
628+
pass
629+
630+
self.assertEqual(C.__abstractmethods__, {'foo'})
631+
632+
del C.foo
633+
634+
abc.update_abstractmethods(C)
635+
636+
self.assertEqual(C.__abstractmethods__, set())
637+
638+
C()
639+
491640

492641
class TestABCWithInitSubclass(unittest.TestCase):
493642
def test_works_with_init_subclass(self):

Lib/test/test_dataclasses.py

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from dataclasses import *
66

7+
import abc
78
import pickle
89
import inspect
910
import builtins
@@ -3332,6 +3333,42 @@ class C:
33323333

33333334
## replace(c, x=5)
33343335

3336+
class TestAbstract(unittest.TestCase):
3337+
def test_abc_implementation(self):
3338+
class Ordered(abc.ABC):
3339+
@abc.abstractmethod
3340+
def __lt__(self, other):
3341+
pass
3342+
3343+
@abc.abstractmethod
3344+
def __le__(self, other):
3345+
pass
3346+
3347+
@dataclass(order=True)
3348+
class Date(Ordered):
3349+
year: int
3350+
month: 'Month'
3351+
day: 'int'
3352+
3353+
self.assertFalse(inspect.isabstract(Date))
3354+
self.assertGreater(Date(2020,12,25), Date(2020,8,31))
3355+
3356+
def test_maintain_abc(self):
3357+
class A(abc.ABC):
3358+
@abc.abstractmethod
3359+
def foo(self):
3360+
pass
3361+
3362+
@dataclass
3363+
class Date(A):
3364+
year: int
3365+
month: 'Month'
3366+
day: 'int'
3367+
3368+
self.assertTrue(inspect.isabstract(Date))
3369+
msg = 'class Date with abstract method foo'
3370+
self.assertRaisesRegex(TypeError, msg, Date)
3371+
33353372

33363373
if __name__ == '__main__':
33373374
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)