diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1493ad46..25e77479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: python -Im coverage html --skip-covered --skip-empty # Report and write to summary. - python -Im coverage report | sed 's/^/ /' >> $GITHUB_STEP_SUMMARY + python -Im coverage report --format=markdown >> $GITHUB_STEP_SUMMARY # Report again and fail if under 100%. python -Im coverage report --fail-under=100 diff --git a/.gitignore b/.gitignore index b3e3cc92..b58afd70 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ tmp* attrs.docset attrs.tgz Justfile +t.py diff --git a/changelog.d/1172.change.md b/changelog.d/1172.change.md new file mode 100644 index 00000000..e3a7b0c2 --- /dev/null +++ b/changelog.d/1172.change.md @@ -0,0 +1,2 @@ +`attrs.AttrsInstance` is now a `typing.Protocol` in both type hints and code. +This allows you to subclass it along with another `Protocol`. diff --git a/pyproject.toml b/pyproject.toml index bbdd6bcd..bdb3a546 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,9 @@ exclude_lines = [ "pragma: no cover", # PyPy is unacceptably slow under coverage. "if PYPY:", + # not meant to be executed + ': \.\.\.$', + '^ +\.\.\.$', ] diff --git a/src/attr/__init__.py b/src/attr/__init__.py index 3ba6c94d..8a7a86d3 100644 --- a/src/attr/__init__.py +++ b/src/attr/__init__.py @@ -9,6 +9,7 @@ from typing import Callable from . import converters, exceptions, filters, setters, validators from ._cmp import cmp_using +from ._compat import Protocol from ._config import get_run_validators, set_run_validators from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types from ._make import ( @@ -31,7 +32,7 @@ ib = attr = attrib dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) -class AttrsInstance: +class AttrsInstance(Protocol): pass diff --git a/src/attr/_compat.py b/src/attr/_compat.py index c3bf5e33..a4572062 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -18,6 +18,15 @@ PY310 = sys.version_info[:2] >= (3, 10) PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +if sys.version_info < (3, 8): + try: + from typing_extensions import Protocol + except ImportError: # pragma: no cover + Protocol = object +else: + from typing import Protocol # noqa: F401 + + def just_warn(*args, **kw): warnings.warn( "Running interpreter doesn't sufficiently support code object " diff --git a/tests/test_compat.py b/tests/test_compat.py index 4a156c4d..c8015b59 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -4,6 +4,8 @@ import types import pytest +import attr + @pytest.fixture(name="mp") def _mp(): @@ -50,3 +52,13 @@ class TestMetadataProxy: with pytest.raises(AttributeError, match="no attribute 'setdefault'"): mp.setdefault("x") + + +def test_attrsinstance_subclass_protocol(): + """ + It's possible to subclass AttrsInstance and Protocol at once. + """ + + class Foo(attr.AttrsInstance, attr._compat.Protocol): + def attribute(self) -> int: + ...