From c58ffd4e4cba5d5e58356722b985fc362358c48e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 11 Aug 2022 13:33:34 +0200 Subject: [PATCH] Call abc.update_abstractmethods on 3.10+ (#1001) --- changelog.d/1001.change.rst | 3 ++ src/attr/_make.py | 34 ++++++++++++++++------ tests/test_abc.py | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 changelog.d/1001.change.rst create mode 100644 tests/test_abc.py diff --git a/changelog.d/1001.change.rst b/changelog.d/1001.change.rst new file mode 100644 index 000000000..262babd4d --- /dev/null +++ b/changelog.d/1001.change.rst @@ -0,0 +1,3 @@ +On Python 3.10 and later, call `abc.update_abstractmethods() `_ on dict classes after creation. +This improves the detection of abstractness. diff --git a/src/attr/_make.py b/src/attr/_make.py index 730ed60cc..bc296c648 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -717,15 +717,33 @@ def __init__( def __repr__(self): return f"<_ClassBuilder(cls={self._cls.__name__})>" - def build_class(self): - """ - Finalize class based on the accumulated configuration. + if PY310: + import abc + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + + return self.abc.update_abstractmethods( + self._patch_original_class() + ) + + else: + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: return self._patch_original_class() def _patch_original_class(self): diff --git a/tests/test_abc.py b/tests/test_abc.py new file mode 100644 index 000000000..d5682b46a --- /dev/null +++ b/tests/test_abc.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: MIT + +import abc +import inspect + +import pytest + +import attrs + +from attr._compat import PY310 + + +@pytest.mark.skipif(not PY310, reason="abc.update_abstractmethods is 3.10+") +class TestUpdateAbstractMethods: + def test_abc_implementation(self, slots): + """ + If an attrs class implements an abstract method, it stops being + abstract. + """ + + class Ordered(abc.ABC): + @abc.abstractmethod + def __lt__(self, other): + pass + + @abc.abstractmethod + def __le__(self, other): + pass + + @attrs.define(order=True, slots=slots) + class Concrete(Ordered): + x: int + + assert not inspect.isabstract(Concrete) + assert Concrete(2) > Concrete(1) + + def test_remain_abstract(self, slots): + """ + If an attrs class inherits from an abstract class but doesn't implement + abstract methods, it remains abstract. + """ + + class A(abc.ABC): + @abc.abstractmethod + def foo(self): + pass + + @attrs.define(slots=slots) + class StillAbstract(A): + pass + + assert inspect.isabstract(StillAbstract) + with pytest.raises( + TypeError, match="class StillAbstract with abstract method foo" + ): + StillAbstract()