From f65b0f8ddc128cf893d595c2e8867e013669bd0a Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Thu, 1 Oct 2020 23:52:12 +0300 Subject: [PATCH 01/20] added update_abstractmethods --- Doc/library/abc.rst | 21 +++++++++++++++----- Lib/abc.py | 32 +++++++++++++++++++++++++++++++ Lib/dataclasses.py | 5 +++++ Lib/functools.py | 11 ++++++++--- Lib/test/test_abc.py | 36 +++++++++++++++++++++++++++++++++++ Lib/test/test_dataclasses.py | 37 ++++++++++++++++++++++++++++++++++++ Lib/test/test_functools.py | 14 ++++++++++++++ 7 files changed, 148 insertions(+), 8 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 424ae547d829a4..73f79308ffbae3 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,17 @@ 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 class decorator. + + Returns *cls*, to allow usage as a class decorator. + + If *cls* has any subclasses, raises a :exc:`RuntimeError`. + + .. versionadded:: 3.10 .. rubric:: Footnotes diff --git a/Lib/abc.py b/Lib/abc.py index 431b64040a66e8..eace519a0f22ed 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -121,6 +121,38 @@ 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. + + 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. Alternativly, if a new abstract method has been + added to the class, it will not be considered to require that method + implemented until this function is called. + + This function should be called before any use is made of the class, + usually in class decorators that implement methods. + + Returns cls, to allow usage as a class decorator. + + If cls is has any subclasses, raises a RuntimeError. + """ + if cls.__subclasses__(): + raise RuntimeError("cannot update abstract method of class after suclassing") + abstracts = set() + # check the existing abstract methods, keep only the ones that are + # still abstract + for name in cls.__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 diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 530d3e99574e8e..4f777bc7b839ae 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -6,6 +6,7 @@ import keyword import builtins import functools +import abc import _thread from types import GenericAlias @@ -992,6 +993,10 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): cls.__doc__ = (cls.__name__ + str(inspect.signature(cls)).replace(' -> None', '')) + # update the abstract methods of the class, if it is abstract + if isinstance(cls, abc.ABCMeta): + abc.update_abstractmethods(cls) + return cls diff --git a/Lib/functools.py b/Lib/functools.py index b1f1fe8d9a6f27..96b4e35efd849f 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -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,8 +187,10 @@ 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)} if not roots: raise ValueError('must define at least one ordering operation: < > <= >=') root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ @@ -196,6 +198,9 @@ def total_ordering(cls): 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 diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 7e9c47b3cacb96..dbc5d264aebeb0 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -487,6 +487,42 @@ class B: ... class C(with_metaclass(abc_ABCMeta, A, B)): pass self.assertEqual(C.__class__, abc_ABCMeta) + + 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()) class TestABCWithInitSubclass(unittest.TestCase): diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index b20103bdce51cb..b31a469ec79227 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 C: ## 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/Lib/test/test_functools.py b/Lib/test/test_functools.py index edd5773e13d549..4c30323d17c5ab 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3,6 +3,7 @@ import collections import collections.abc import copy +import inspect from itertools import permutations import pickle from random import choice @@ -1157,6 +1158,19 @@ def test_pickle(self): method_copy = pickle.loads(pickle.dumps(method, proto)) self.assertIs(method_copy, method) + def test_abc_impl(self): + class A(abc.ABC): + @abc.abstractmethod + def __gt__(self, other): + pass + + class B(A): + def __lt__(self, other): + return True + B = functools.total_ordering(B) + B() + self.assertFalse(inspect.isabstract(B)) + @functools.total_ordering class Orderable_LT: def __init__(self, value): From c04ba74aa04285d7046ed8bad74f1b4ce0204312 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 21:11:04 +0000 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst 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 00000000000000..4a0cffc5bf67ed --- /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* and *total_ordering* have been changed to call this function. Finally, *total_ordering* was patched so that it would override abstract methods. \ No newline at end of file From ea69ae6ccff94a45896e14ac319470b1fa0441cf Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 00:24:46 +0300 Subject: [PATCH 03/20] fix whitespaces --- Lib/abc.py | 8 ++++---- Lib/test/test_abc.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/abc.py b/Lib/abc.py index eace519a0f22ed..7a49463ac9c7f4 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -124,18 +124,18 @@ def _abc_caches_clear(cls): def update_abstractmethods(cls): """Repopulate the abstract methods of an abstract class, or a subclass 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. Alternativly, if a new abstract method has been added to the class, it will not be considered to require that method implemented until this function is called. - + This function should be called before any use is made of the class, usually in class decorators that implement methods. - + Returns cls, to allow usage as a class decorator. - + If cls is has any subclasses, raises a RuntimeError. """ if cls.__subclasses__(): diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index dbc5d264aebeb0..5424d1ca0afa20 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -487,17 +487,17 @@ class B: ... class C(with_metaclass(abc_ABCMeta, A, B)): pass self.assertEqual(C.__class__, abc_ABCMeta) - + 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" @@ -509,18 +509,18 @@ 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()) From 61c4f40ede01206ccfdb5b78005299e927ad1245 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 00:30:54 +0300 Subject: [PATCH 04/20] documentation fixes --- Doc/library/abc.rst | 2 +- Lib/abc.py | 8 ++++---- Lib/dataclasses.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 73f79308ffbae3..8c8e40008577c1 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -339,7 +339,7 @@ The :mod:`abc` module also provides the following functions: 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 class decorator. + be called from within a class decorator. Returns *cls*, to allow usage as a class decorator. diff --git a/Lib/abc.py b/Lib/abc.py index 7a49463ac9c7f4..9e952d6e3c4d5d 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -128,18 +128,18 @@ def update_abstractmethods(cls): 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. Alternativly, if a new abstract method has been - added to the class, it will not be considered to require that method - implemented until this function is called. + 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 implement methods. + 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.__subclasses__(): - raise RuntimeError("cannot update abstract method of class after suclassing") + raise RuntimeError("cannot update abstract methods of class after subclassing") abstracts = set() # check the existing abstract methods, keep only the ones that are # still abstract diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 4f777bc7b839ae..a69155c815e876 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -993,7 +993,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): cls.__doc__ = (cls.__name__ + str(inspect.signature(cls)).replace(' -> None', '')) - # update the abstract methods of the class, if it is abstract + # Update the abstract methods of the class, if it is abstract. if isinstance(cls, abc.ABCMeta): abc.update_abstractmethods(cls) From a5a9fd7e941bfe53a4178f75e6e312f287ba0771 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 01:08:55 +0300 Subject: [PATCH 05/20] additional tests --- Lib/test/test_abc.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 5424d1ca0afa20..02c684900d86c0 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -488,6 +488,22 @@ 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 @@ -524,6 +540,24 @@ class B(A): B() self.assertEqual(B.__abstractmethods__, set()) + def test_update_asdecorator(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()) + class TestABCWithInitSubclass(unittest.TestCase): def test_works_with_init_subclass(self): From 5e1fffc91bd0cf0fb2bb1cfc03061de9910452ca Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 01:09:25 +0300 Subject: [PATCH 06/20] rename test --- Lib/test/test_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 02c684900d86c0..e0f0cd330a4e46 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -540,7 +540,7 @@ class B(A): B() self.assertEqual(B.__abstractmethods__, set()) - def test_update_asdecorator(self): + def test_update_as_decorator(self): class A(metaclass=abc_ABCMeta): @abc.abstractmethod def foo(self): From 2f95b840e24e1f2e358035633f5aa156d15e2ee6 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 01:26:34 +0300 Subject: [PATCH 07/20] added explicit check for ABCMeta --- Doc/library/abc.rst | 2 ++ Lib/abc.py | 20 +++++++++++++++----- Lib/test/test_abc.py | 11 +++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 8c8e40008577c1..aeb3c5cba168f6 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -345,6 +345,8 @@ The :mod:`abc` module also provides the following functions: If *cls* has any subclasses, raises a :exc:`RuntimeError`. + If *cls* is not an instance of ABCMeta, raises a :exc:`TypeError`. + .. versionadded:: 3.10 .. rubric:: Footnotes diff --git a/Lib/abc.py b/Lib/abc.py index 9e952d6e3c4d5d..3a27404f7ed41a 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -127,7 +127,7 @@ def update_abstractmethods(cls): 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. Alternativly, if a new abstract method has been + 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. @@ -137,17 +137,27 @@ class after this function is called. 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") + raise RuntimeError("cannot update abstract methods of class after" + " subclassing") + abstracts = set() - # check the existing abstract methods, keep only the ones that are - # still abstract + # Check the existing abstract methods, keep only the ones that are + # still abstract. for name in cls.__abstractmethods__: value = getattr(cls, name, None) if getattr(value, "__isabstractmethod__", False): abstracts.add(name) - # also add any other newly added abstract methods + # Also add any other newly added abstract methods. for name, value in cls.__dict__.items(): if getattr(value, "__isabstractmethod__", False): abstracts.add(name) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index e0f0cd330a4e46..7cd2465a585784 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -558,6 +558,17 @@ class B(A): 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): From 3986c98fa696674604f92f2fcdc236fcfa0eaa83 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Fri, 2 Oct 2020 15:37:26 +0300 Subject: [PATCH 08/20] optimization on total_ordering --- Lib/functools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 96b4e35efd849f..296826f9472b4a 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -189,8 +189,8 @@ def total_ordering(cls): """Class decorator that fills in missing ordering methods""" # 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)} + if (root := getattr(cls, op, None)) is not getattr(object, op, None) + and not getattr(root, '__isabstractmethod__', False)} if not roots: raise ValueError('must define at least one ordering operation: < > <= >=') root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ From 4f3d8469c2afec3bfcae11b0e6502f8d3c44bf46 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sat, 3 Oct 2020 12:05:26 +0300 Subject: [PATCH 09/20] update_abstractmethods now ignores non-abstract classes --- Doc/library/abc.rst | 2 +- Lib/abc.py | 6 +++--- Lib/dataclasses.py | 4 +--- Lib/test/test_abc.py | 4 +++- .../next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index aeb3c5cba168f6..757a54ff584683 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -345,7 +345,7 @@ The :mod:`abc` module also provides the following functions: If *cls* has any subclasses, raises a :exc:`RuntimeError`. - If *cls* is not an instance of ABCMeta, raises a :exc:`TypeError`. + If *cls* is not an instance of ABCMeta, does nothing. .. versionadded:: 3.10 diff --git a/Lib/abc.py b/Lib/abc.py index 3a27404f7ed41a..8338a47c5d5dd1 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -138,14 +138,14 @@ class after this function is called. If cls is has any subclasses, raises a RuntimeError. - If cls is not an instance of ABCMeta, raises a TypeError. + 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. - raise TypeError('cls must be an abstract class or subclass of an abstract' - ' class') + return cls + if cls.__subclasses__(): raise RuntimeError("cannot update abstract methods of class after" " subclassing") diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index a69155c815e876..65091021f37162 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -993,9 +993,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): cls.__doc__ = (cls.__name__ + str(inspect.signature(cls)).replace(' -> None', '')) - # Update the abstract methods of the class, if it is abstract. - if isinstance(cls, abc.ABCMeta): - abc.update_abstractmethods(cls) + abc.update_abstractmethods(cls) return cls diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 7cd2465a585784..0c60f749eee56b 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -567,7 +567,9 @@ def updated_foo(self): pass A.foo = updated_foo - self.assertRaises(TypeError, abc.update_abstractmethods, A) + abc.update_abstractmethods(A) + A() + self.assertFalse(hasattr(A, '__abstractmethods__')) class TestABCWithInitSubclass(unittest.TestCase): 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 index 4a0cffc5bf67ed..82fceca03a623d 100644 --- 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 @@ -1 +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. \ No newline at end of file +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 defined in superclasses. \ No newline at end of file From 74473c83f6619b3da7fc984b63d04d0a07020c09 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sat, 3 Oct 2020 12:05:56 +0300 Subject: [PATCH 10/20] total_ordering now only overrides abtract methods defined in superclasses --- Lib/functools.py | 20 +++++++++++++------- Lib/test/test_functools.py | 24 +++++++++++++++++++++++- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 296826f9472b4a..f23ffad56eeeb9 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -187,10 +187,18 @@ 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 or abstract). - roots = {op for op in _convert - if (root := getattr(cls, op, None)) is not getattr(object, op, None) - and not getattr(root, '__isabstractmethod__', False)} + # Find user-defined comparisons (not those inherited from object or + # abstract). + def is_root(op): + root = getattr(cls, op, None) + if root is getattr(object, op, None): + return False + if getattr(root, '__isabstractmethod__', False): + # We only accept a root if it defined in the class itself. + return op in cls.__dict__ + return True + + roots = {op for op in _convert if is_root(op)} if not roots: raise ValueError('must define at least one ordering operation: < > <= >=') root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ @@ -198,9 +206,7 @@ def total_ordering(cls): 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) + update_abstractmethods(cls) return cls diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 4c30323d17c5ab..530d5afa937dea 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -1158,16 +1158,38 @@ def test_pickle(self): method_copy = pickle.loads(pickle.dumps(method, proto)) self.assertIs(method_copy, method) + def test_abc_forward(self): + @functools.total_ordering + class A(abc.ABC): + @abc.abstractmethod + def __lt__(self, other): + pass + + self.assertLess({'__lt__', '__le__', '__gt__', '__ge__'}, A.__dict__.keys()) + self.assertTrue(inspect.isabstract(A)) + self.assertEqual(A.__abstractmethods__, {'__lt__'}) + + class Inf(A): + def __lt__(self, other): + return False + + self.assertFalse(inspect.isabstract(Inf)) + self.assertTrue(Inf().__gt__(None)) + def test_abc_impl(self): class A(abc.ABC): @abc.abstractmethod def __gt__(self, other): pass + @abc.abstractmethod + def __lt__(self, other): + pass + + @functools.total_ordering class B(A): def __lt__(self, other): return True - B = functools.total_ordering(B) B() self.assertFalse(inspect.isabstract(B)) From 92326c9a23918a8b6ecb3b2e16b625c57d613464 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sat, 3 Oct 2020 18:25:25 +0300 Subject: [PATCH 11/20] doc fix --- Lib/functools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index f23ffad56eeeb9..14a13ca907175b 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -194,7 +194,8 @@ def is_root(op): if root is getattr(object, op, None): return False if getattr(root, '__isabstractmethod__', False): - # We only accept a root if it defined in the class itself. + # We only accept an abstract root if it defined in the class + # itself. return op in cls.__dict__ return True From 1e47ee48a0086e27ca7317f2bcc3f2e13749173a Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sat, 3 Oct 2020 19:02:29 +0300 Subject: [PATCH 12/20] excluded total_ordering from this issue --- Lib/functools.py | 18 +++--------------- Lib/test/test_functools.py | 36 ------------------------------------ 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 14a13ca907175b..b1f1fe8d9a6f27 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -14,7 +14,7 @@ 'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod', 'cached_property'] -from abc import ABCMeta, get_cache_token, update_abstractmethods +from abc import get_cache_token from collections import namedtuple # import types, weakref # Deferred to single_dispatch() from reprlib import recursive_repr @@ -187,19 +187,8 @@ 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 or - # abstract). - def is_root(op): - root = getattr(cls, op, None) - if root is getattr(object, op, None): - return False - if getattr(root, '__isabstractmethod__', False): - # We only accept an abstract root if it defined in the class - # itself. - return op in cls.__dict__ - return True - - roots = {op for op in _convert if is_root(op)} + # 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)} if not roots: raise ValueError('must define at least one ordering operation: < > <= >=') root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ @@ -207,7 +196,6 @@ def is_root(op): if opname not in roots: opfunc.__name__ = opname setattr(cls, opname, opfunc) - update_abstractmethods(cls) return cls diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 530d5afa937dea..edd5773e13d549 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3,7 +3,6 @@ import collections import collections.abc import copy -import inspect from itertools import permutations import pickle from random import choice @@ -1158,41 +1157,6 @@ def test_pickle(self): method_copy = pickle.loads(pickle.dumps(method, proto)) self.assertIs(method_copy, method) - def test_abc_forward(self): - @functools.total_ordering - class A(abc.ABC): - @abc.abstractmethod - def __lt__(self, other): - pass - - self.assertLess({'__lt__', '__le__', '__gt__', '__ge__'}, A.__dict__.keys()) - self.assertTrue(inspect.isabstract(A)) - self.assertEqual(A.__abstractmethods__, {'__lt__'}) - - class Inf(A): - def __lt__(self, other): - return False - - self.assertFalse(inspect.isabstract(Inf)) - self.assertTrue(Inf().__gt__(None)) - - def test_abc_impl(self): - class A(abc.ABC): - @abc.abstractmethod - def __gt__(self, other): - pass - - @abc.abstractmethod - def __lt__(self, other): - pass - - @functools.total_ordering - class B(A): - def __lt__(self, other): - return True - B() - self.assertFalse(inspect.isabstract(B)) - @functools.total_ordering class Orderable_LT: def __init__(self, value): From 740183fe8a03abf9b4814c2a942198020e5dd019 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sat, 3 Oct 2020 19:08:33 +0300 Subject: [PATCH 13/20] documented ABC being ABC unaware --- Doc/library/functools.rst | 7 +++++++ .../next/Library/2020-10-01-21-11-03.bpo-41905._JpjR4.rst | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 14aa184e2cd14c..186cb4c381dee4 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/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 index 82fceca03a623d..0d8c0ba6a66bd1 100644 --- 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 @@ -1 +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 defined in superclasses. \ No newline at end of file +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 From 114028e620423691313342100f841abc2bd9b27a Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sun, 4 Oct 2020 20:36:08 +0300 Subject: [PATCH 14/20] fixed potential bug with deletion of implementation --- Lib/abc.py | 13 +++++++------ Lib/test/test_abc.py | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Lib/abc.py b/Lib/abc.py index 8338a47c5d5dd1..f11397152fbbba 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -151,12 +151,13 @@ class after this function is called. " subclassing") abstracts = set() - # Check the existing abstract methods, keep only the ones that are - # still abstract. - for name in cls.__abstractmethods__: - value = getattr(cls, name, None) - if getattr(value, "__isabstractmethod__", False): - abstracts.add(name) + # 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): diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 0c60f749eee56b..ffacc7f4b0b0ea 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -516,8 +516,8 @@ def updated_foo(self): A.foo = updated_foo abc.update_abstractmethods(A) - msg = "class A with abstract methods bar, foo" self.assertEqual(A.__abstractmethods__, {'foo', 'bar'}) + msg = "class A with abstract methods bar, foo" self.assertRaisesRegex(TypeError, msg, A) def test_update_implementation(self): @@ -571,6 +571,26 @@ def updated_foo(self): 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) + + class TestABCWithInitSubclass(unittest.TestCase): def test_works_with_init_subclass(self): From ba8df080f0f4ccb092e73d13098b6a6cf675c6e0 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Sun, 4 Oct 2020 21:04:49 +0300 Subject: [PATCH 15/20] added layered test case --- Lib/test/test_abc.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index ffacc7f4b0b0ea..60276b1cfa0ec9 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -590,6 +590,28 @@ def foo(self): 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) + class TestABCWithInitSubclass(unittest.TestCase): From 29dba375c53e5558c68e248cc9f14fb4cb648720 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Mon, 5 Oct 2020 01:36:43 +0300 Subject: [PATCH 16/20] update_abstractmethods doesn't care about subclasses for now --- Doc/library/abc.rst | 10 ++++++++-- Lib/abc.py | 9 +-------- Lib/test/test_abc.py | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 757a54ff584683..b5de2ea632bcb7 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -343,10 +343,16 @@ The :mod:`abc` module also provides the following functions: Returns *cls*, to allow usage as a class decorator. - If *cls* has any subclasses, raises a :exc:`RuntimeError`. - If *cls* is not an instance of ABCMeta, does nothing. + .. note:: + + Since it is meant to be called before the class is used by anywhere but + in the decorator that called it, this function assumes that *cls* has no + subclasses. This means that if the decorator itself creates subclasses + to *cls*, it must also call *update_abstractmethods* on each of them. + + .. versionadded:: 3.10 .. rubric:: Footnotes diff --git a/Lib/abc.py b/Lib/abc.py index f11397152fbbba..729aab730d1cd1 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -122,8 +122,7 @@ def _abc_caches_clear(cls): _reset_caches(cls) def update_abstractmethods(cls): - """Repopulate the abstract methods of an abstract class, or a subclass of - an abstract class. + """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 @@ -136,8 +135,6 @@ class after this function is called. 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, does nothing. """ if not hasattr(cls, '__abstractmethods__'): @@ -146,10 +143,6 @@ class after this function is called. # testing), and we want to handle both cases. return cls - if cls.__subclasses__(): - raise RuntimeError("cannot update abstract methods of class after" - " subclassing") - abstracts = set() # Check the existing abstract methods of the parents, keep only the ones # that are not implemented. diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 60276b1cfa0ec9..3d603e7734d870 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -612,6 +612,30 @@ def foo(self): 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): From 779e6cfa6572573894ccd03249f02a3bcbed1843 Mon Sep 17 00:00:00 2001 From: ben avrahami Date: Mon, 5 Oct 2020 01:51:47 +0300 Subject: [PATCH 17/20] doc fixes --- Doc/library/abc.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index b5de2ea632bcb7..40d401e2065592 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -347,11 +347,13 @@ The :mod:`abc` module also provides the following functions: .. note:: - Since it is meant to be called before the class is used by anywhere but - in the decorator that called it, this function assumes that *cls* has no - subclasses. This means that if the decorator itself creates subclasses - to *cls*, it must also call *update_abstractmethods* on each of them. + This function ignores whatever subclasses *cls* might have. This means + that if the decorator itself creates subclasses to *cls*, it must also + call *update_abstractmethods* on each of them. + .. note:: + + This function assumes that *cls*'s superclasses are already updated. .. versionadded:: 3.10 From eea97f4b3b7eae44b2b5e23a90822cdf7eb17cfd Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Mon, 5 Oct 2020 12:56:14 +0300 Subject: [PATCH 18/20] Update Doc/library/abc.rst Co-authored-by: Guido van Rossum --- Doc/library/abc.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Doc/library/abc.rst b/Doc/library/abc.rst index 40d401e2065592..3a7414d7358e7a 100644 --- a/Doc/library/abc.rst +++ b/Doc/library/abc.rst @@ -345,15 +345,10 @@ The :mod:`abc` module also provides the following functions: If *cls* is not an instance of ABCMeta, does nothing. - .. note:: - - This function ignores whatever subclasses *cls* might have. This means - that if the decorator itself creates subclasses to *cls*, it must also - call *update_abstractmethods* on each of them. - .. note:: This function assumes that *cls*'s superclasses are already updated. + It does not update any subclasses. .. versionadded:: 3.10 From 8eec6c2aac208a668b612cedeb9c13d7ace1c986 Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Mon, 5 Oct 2020 12:57:24 +0300 Subject: [PATCH 19/20] Update Lib/abc.py Co-authored-by: Guido van Rossum --- Lib/abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/abc.py b/Lib/abc.py index 729aab730d1cd1..7bffa133fad970 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -121,6 +121,7 @@ def _abc_caches_clear(cls): """Clear the caches (for debugging or testing).""" _reset_caches(cls) + def update_abstractmethods(cls): """Recalculate the set of abstract methods of an abstract class. From 56981535ea4578ae00264cf2ed502f62becada04 Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Mon, 5 Oct 2020 12:57:40 +0300 Subject: [PATCH 20/20] Update Lib/abc.py Co-authored-by: Guido van Rossum --- Lib/abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/abc.py b/Lib/abc.py index 7bffa133fad970..276ef9a2cd4850 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -159,6 +159,7 @@ class after this function is called. cls.__abstractmethods__ = frozenset(abstracts) return cls + class ABC(metaclass=ABCMeta): """Helper class that provides a standard way to create an ABC using inheritance.