Repository: python-attrs/attrs (python)

Issue:
python3.10 interacts badly with typing_extensions.ClassVar
```python
from typing import Callable, Optional, ClassVar

import typing_extensions
import attr


@attr.s(auto_attribs=True)
class Ham:
    ham: typing_extensions.ClassVar[int] = 0
    spam: int
    eggs: Optional[int] = attr.ib(init=False, default=None)
```

```pytb
python3.10 foo.py
Traceback (most recent call last):
  File "/home/graingert/projects/twisted/foo.py", line 8, in <module>
    class Ham:
  File "/home/graingert/projects/twisted/.tox/py310-alldeps-nocov/lib/python3.10/site-packages/attr/_make.py", line 1292, in wrap
    builder = _ClassBuilder(
  File "/home/graingert/projects/twisted/.tox/py310-alldeps-nocov/lib/python3.10/site-packages/attr/_make.py", line 604, in __init__
    attrs, base_attrs, base_map = _transform_attrs(
  File "/home/graingert/projects/twisted/.tox/py310-alldeps-nocov/lib/python3.10/site-packages/attr/_make.py", line 518, in _transform_attrs
    raise ValueError(
ValueError: No mandatory attributes allowed after an attribute with a default value or factory.  Attribute in question: Attribute(name='spam', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type='int', converter=None, kw_only=False, inherited=False, on_setattr=None)
```

Pull request:
Detect ClassVars coming from typing_extensions
Fixes #781

@graingert @euresti 

Patch:
diff --git a/changelog.d/782.change.rst b/changelog.d/782.change.rst
new file mode 100644
index 0000000..9314ccf
--- /dev/null
+++ b/changelog.d/782.change.rst
@@ -0,0 +1,1 @@
+``ClassVar``\ s are now also detected if they come from `typing-extensions <https://pypi.org/project/typing-extensions/>`_.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 3b02bbb..d0a2511 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -41,7 +41,12 @@ _init_factory_pat = "__attr_factory_{}"
 _tuple_property_pat = (
     "    {attr_name} = _attrs_property(_attrs_itemgetter({index}))"
 )
-_classvar_prefixes = ("typing.ClassVar", "t.ClassVar", "ClassVar")
+_classvar_prefixes = (
+    "typing.ClassVar",
+    "t.ClassVar",
+    "ClassVar",
+    "typing_extensions.ClassVar",
+)
 # we don't use a double-underscore prefix because that triggers
 # name mangling when trying to create a slot for the field
 # (when slots=True)


Test patch:
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
index 203b8dd..2df6311 100644
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -418,6 +418,18 @@ class TestAnnotations:
             foo=typing.Optional[typing.Any],
         )
 
+    @pytest.mark.parametrize("slots", [True, False])
+    def test_typing_extensions_classvar(self, slots):
+        """
+        If ClassVar is coming from typing_extensions, it is recognized too.
+        """
+
+        @attr.s(auto_attribs=True, slots=slots)
+        class C:
+            cls_var: "typing_extensions.ClassVar" = 23  # noqa
+
+        assert_init_annotations(C)
+
     def test_keyword_only_auto_attribs(self):
         """
         `kw_only` propagates to attributes defined via `auto_attribs`.