Repository: python-attrs/attrs

Problem statement:
Support ClassVar string annotations
The following doesn't work in Python 3.7

```
from __future__ import annotations
import attr
from typing import ClassVar

@attr.dataclass
class A:
    x: ClassVar[int]

a = A()
```

Because `ClassVar[int]` will be `'ClassVar[int]` in `A.__annotations__` and so `return str(annot).startswith("typing.ClassVar")` returns `False`.

You can repro without 3.7 using this:

```
import attr
from typing import ClassVar

@attr.dataclass
class A:
    x: 'ClassVar[int]'

a = A()
```

Note: It's weird I'm not giving it a value but if I do then attrs will think it's a default value and the code won't error out.

Oh and the following works fine:
```
import attr
import typing

@attr.dataclass
class A:
    x: 'typing.ClassVar[int]'

a = A()
```



Hints:
Oh god more string comparisons. 🙈
This is the last blocker for the next release. Any ideas how to solve this save expanding the comparison to just ClassVar and call it a day?
I believe these are our only viable options:

1. `eval` the first part (up to the first `[`) of every string annotation.  
* If it raises, move on, if you get something check if it's a ClassVar.
* Pros:  Catches every possible invocation, `from typing import ClassVar as MyClassVar`
* Cons: I imagine this is prohibitively slow but it could be tested.
  
2. Just check for `.startswith('ClassVar')` also and move on.
* Pros: Fast.
* Cons: This will miss `as` imports.  And will also ignore situations in which you didn't import ClassVar correctly or at all.  e.g.
```
@attr.s(auto_attrib=True)
class a:
    a: 'ClassVar'
```
(Though any good linter would catch that)

Can I haz a non-terrible third option? :'(
The non-gross option is to import typing and use `get_type_hints()`. I thought you're already doing that for resolving regular fields but apparently not.

How about this: if user code *is* using annotations *and* that annotation is a string, *then* we import typing and use `get_type_hints()` like grown-ups? In all likelihood if the code was already using annotations, it was importing typing somewhere anyway so there's no danger of slowing anybody down. Plus, since user code is already using annotations, we know it's Python 3 so `typing` is there?

Let me create a pull request to that effect.
OK, so I tried the above and it seems it's a nuclear option since it forces all annotations to be evaluated. This is what I wanted to avoid with `from __future__ import annotations`.

Making it more intelligent would be possible with `typing.ForwardRef` and its `_evaluate()` method but those are only available on 3.7+ (PEP 560).

I looked what `dataclasses` do in this case and they also don't work with `from __future__ import annotations` which I need to fix before Python 3.7 beta4.

So my pull request is going with Euresti's Option 2. This is going to be enough for 99.9% users.

Patch:
diff --git a/src/attr/_make.py b/src/attr/_make.py
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -23,6 +23,8 @@
 _init_converter_pat = "__attr_converter_{}"
 _init_factory_pat = "__attr_factory_{}"
 _tuple_property_pat = "    {attr_name} = property(itemgetter({index}))"
+_classvar_prefixes = ("typing.ClassVar", "t.ClassVar", "ClassVar")
+
 _empty_metadata_singleton = metadata_proxy({})
 
 
@@ -232,10 +234,11 @@ def _is_class_var(annot):
     """
     Check whether *annot* is a typing.ClassVar.
 
-    The implementation is gross but importing `typing` is slow and there are
-    discussions to remove it from the stdlib alltogether.
+    The string comparison hack is used to avoid evaluating all string
+    annotations which would put attrs-based classes at a performance
+    disadvantage compared to plain old classes.
     """
-    return str(annot).startswith("typing.ClassVar")
+    return str(annot).startswith(_classvar_prefixes)
 
 
 def _get_annotations(cls):


Test patch:
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -11,6 +11,7 @@
 
 import attr
 
+from attr._make import _classvar_prefixes
 from attr.exceptions import UnannotatedAttributeError
 
 
@@ -204,13 +205,14 @@ class A:
         assert A.__init__.__annotations__ == {'return': None}
 
     @pytest.mark.parametrize("slots", [True, False])
-    def test_annotations_strings(self, slots):
+    @pytest.mark.parametrize("classvar", _classvar_prefixes)
+    def test_annotations_strings(self, slots, classvar):
         """
         String annotations are passed into __init__ as is.
         """
         @attr.s(auto_attribs=True, slots=slots)
         class C:
-            cls_var: 'typing.ClassVar[int]' = 23
+            cls_var: classvar + '[int]' = 23
             a: 'int'
             x: 'typing.List[int]' = attr.Factory(list)
             y: 'int' = 2