From 52fb5c80e463f3c25e2ec1640379dccbc7ebe7a9 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 11 Aug 2022 07:44:20 +0200 Subject: [PATCH 1/3] Call abc.update_abstractmethods on 3.10+ --- conftest.py | 7 ++++++ src/attr/_make.py | 32 ++++++++++++++++++++------- tests/test_abc.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/test_abc.py diff --git a/conftest.py b/conftest.py index f6e5d8d6c..cb9e24205 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,17 @@ # SPDX-License-Identifier: MIT +import pytest + from hypothesis import HealthCheck, settings from attr._compat import PY310 +@pytest.fixture(name="slots", params=(True, False)) +def slots(request): + return request.param + + def pytest_configure(config): # HealthCheck.too_slow causes more trouble than good -- especially in CIs. settings.register_profile( diff --git a/src/attr/_make.py b/src/attr/_make.py index 730ed60cc..c5c5e1d21 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: MIT +import abc import copy import linecache import sys @@ -717,15 +718,30 @@ 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: + + 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 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() From 28154f356907f695fd13251125c7e8ea44b1f647 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 11 Aug 2022 07:53:24 +0200 Subject: [PATCH 2/3] Add newsfragment --- changelog.d/1001.change.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1001.change.rst 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. From 562e7ae98a3489abaa115827653ced8c537fd47f Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Thu, 11 Aug 2022 08:42:08 +0200 Subject: [PATCH 3/3] Make abc import conditional --- src/attr/_make.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index c5c5e1d21..bc296c648 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: MIT -import abc import copy import linecache import sys @@ -719,6 +718,7 @@ def __repr__(self): return f"<_ClassBuilder(cls={self._cls.__name__})>" if PY310: + import abc def build_class(self): """ @@ -729,7 +729,9 @@ def build_class(self): if self._slots is True: return self._create_slots_class() - return abc.update_abstractmethods(self._patch_original_class()) + return self.abc.update_abstractmethods( + self._patch_original_class() + ) else: