Repository: python-attrs/attrs (python)

Issue:
AttrsInstance is only a Protocol in mypy
`__init__.pyi` defines AttrsInstance as follows:
```
class AttrsInstance(AttrsInstance_, Protocol):
    pass
```

So the following typechecks correctly:
```
import attr
from typing import Protocol

class Foo(attr.AttrsInstance, Protocol):
    def attribute(self) -> int:
        ...


def bar(foo: Foo) -> int:
    return foo.attribute()

@attr.define
class CFoo:
    a: int

    def attribute(self) -> int:
        return self.a

bar(CFoo(15))
```

But if you run it you get:

`TypeError: Protocols can only inherit from other protocols, got <class 'attr.AttrsInstance'>`

Because in `__init__.py` it's defined as:
```
class AttrsInstance:
    pass
```

instead of 
```
class AttrsInstance(Protocol):
    pass
```

Pull request:
Allow subclassing of AttrsInstance and Protocol at once
Fixes #1133

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1493ad4..25e7747 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 b3e3cc9..b58afd7 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 0000000..e3a7b0c
--- /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 bbdd6bc..bdb3a54 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 3ba6c94..8a7a86d 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 c3bf5e3..a457206 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 "


Test patch:
diff --git a/tests/test_compat.py b/tests/test_compat.py
index 4a156c4..c8015b5 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:
+            ...