-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
bpo-41905: added abc.update_abstractmethods #22485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
f65b0f8
c04ba74
ea69ae6
8b76715
61c4f40
a5a9fd7
5e1fffc
2f95b84
3986c98
4f3d846
74473c8
92326c9
1e47ee4
740183f
114028e
ba8df08
29dba37
779e6cf
eea97f4
8eec6c2
5698153
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -121,6 +121,48 @@ def _abc_caches_clear(cls): | |||||||
"""Clear the caches (for debugging or testing).""" | ||||||||
_reset_caches(cls) | ||||||||
|
||||||||
def update_abstractmethods(cls): | ||||||||
"""Repopulate the abstract methods of an abstract class, or a subclass of | ||||||||
an abstract class. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like the summary to fit on one line.
Suggested change
|
||||||||
|
||||||||
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 has any subclasses, raises a RuntimeError. | ||||||||
|
||||||||
If cls is not an instance of ABCMeta, raises a TypeError. | ||||||||
""" | ||||||||
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. | ||||||||
raise TypeError('cls must be an abstract class or subclass of an abstract' | ||||||||
' class') | ||||||||
if cls.__subclasses__(): | ||||||||
raise RuntimeError("cannot update abstract methods of class after" | ||||||||
" subclassing") | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I like this. This function is potentially expensive. It could even be detrimental -- I could imagine some very dynamic code that's messing around dynamically with a base class and then calls There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That might be okay, However, removing this check means that we have to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only if you're not sure that there are no subclasses. In I recommend that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that could cause a problem if user chain two decorators: one that updates subclasses and one that doesn't def decoratorA(cls):
"""implements methods and creates a subclass, calling
update_abstractmethods on cls and its subclasses
"""
...
def decoratorB(cls):
"""implements methods only, not updating subclasses
"""
...
@decoratorB
@decoratorA
class A(SomeABC):
...
# A's subclass is now out of sync with decoratorB's changes Thinking about it, the previous implementation was indeed faulty since, even if a decorator that needed to subclass The only sensible solution is to call update_abstractmethods on all of the class's subclasses with each call ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do that, you get what you deserve. Please keep it simple. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's implemented in the latest commit, but I must say I'm not sure what you mean. Who "gets what they deserve"?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Truth be told, if we do assume that decorators and tools are allowed to subclass (and perhaps other permanent operations like instancing) before calling |
||||||||
|
||||||||
abstracts = set() | ||||||||
# Check the existing abstract methods, keep only the ones that are | ||||||||
# still abstract. | ||||||||
for name in cls.__abstractmethods__: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you didn't check in this function that it's an ABC, it's not guaranteed that
Maybe add a test for when this function is called for a non-ABC. |
||||||||
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 | ||||||||
bentheiii marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
class ABC(metaclass=ABCMeta): | ||||||||
"""Helper class that provides a standard way to create an ABC using | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod', | ||
'cached_property'] | ||
|
||
from abc import get_cache_token | ||
from abc import ABCMeta, get_cache_token, update_abstractmethods | ||
from collections import namedtuple | ||
# import types, weakref # Deferred to single_dispatch() | ||
from reprlib import recursive_repr | ||
|
@@ -187,15 +187,20 @@ def _lt_from_ge(self, other, NotImplemented=NotImplemented): | |
|
||
def total_ordering(cls): | ||
"""Class decorator that fills in missing ordering methods""" | ||
# Find user-defined comparisons (not those inherited from object). | ||
roots = {op for op in _convert if getattr(cls, op, None) is not getattr(object, op, None)} | ||
# Find user-defined comparisons (not those inherited from object or abstract). | ||
roots = {op for op in _convert | ||
if getattr(cls, op, None) is not getattr(object, op, None) | ||
and not getattr(getattr(cls, op, None), '__isabstractmethod__', False)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nested getattr() isn't very clear. Also, since this is in the inner loop for a dynamic decorator, it will slow the class creation process. I didn't used to think that was important but experience with named tuples show that people care a great deal about the amount of computation that occurs during an import. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would using a walrus operator be okay here? roots = {...
if (root:=getattr(cls, op, None)) is not getattr(object, op, None)
and not getattr(root, '__isabstractmethod__', False)} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems fine to me. |
||
if not roots: | ||
raise ValueError('must define at least one ordering operation: < > <= >=') | ||
root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ | ||
for opname, opfunc in _convert[root]: | ||
if opname not in roots: | ||
opfunc.__name__ = opname | ||
setattr(cls, opname, opfunc) | ||
# update the abstract methods of the class, if it is abstract | ||
if isinstance(cls, ABCMeta): | ||
update_abstractmethods(cls) | ||
return cls | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -488,6 +488,87 @@ 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) | ||
msg = "class A with abstract methods bar, foo" | ||
self.assertEqual(A.__abstractmethods__, {'foo', 'bar'}) | ||
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()) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you need this test case to cover the first loop:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @iritkatriel What does that test case accomplish other than showing that FooABC is now in an inconsistent state? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum In this comment I left out the update_abstractmethods, see Ben's version which he committed as test_update_del. Without this test the first loop in update_abstractmethods was not exercised. |
||
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 | ||
self.assertRaises(TypeError, abc.update_abstractmethods, A) | ||
|
||
|
||
class TestABCWithInitSubclass(unittest.TestCase): | ||
def test_works_with_init_subclass(self): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
A new function in abc: *update_abstractmethods* to re-calculate an abstract class's abstract status. In addition, *dataclass* and *total_ordering* have been changed to call this function. Finally, *total_ordering* was patched so that it would override abstract methods. |
Uh oh!
There was an error while loading. Please reload this page.