Skip to content
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

Call abc.update_abstractmethods on 3.10+ #1001

Merged
merged 5 commits into from Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/1001.change.rst
@@ -0,0 +1,3 @@
On Python 3.10 and later, call `abc.update_abstractmethods() <https://docs.pytho.
org/3.10/library/abc.html#abc.update_abstractmethods>`_ on dict classes after creation.
This improves the detection of abstractness.
34 changes: 26 additions & 8 deletions src/attr/_make.py
Expand Up @@ -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):
Expand Down
56 changes: 56 additions & 0 deletions 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()