From 7cb8c827620c35404edb473382b69e722209dc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Mon, 23 Apr 2018 06:54:47 -0700 Subject: [PATCH] Support `from typing import ClassVar` (#367) * Support `from typing import ClassVar` Also updated the docstring to reflect why exactly we're doing what we're doing. The previous comment was incorrect (`typing` is already imported in applications using annotations). I added `t.ClassVar` as well which is the only third alternative import I found in use for typing-related classes. Fixes #361 * Tests, docstrings, et al. --- src/attr/_make.py | 9 ++++++--- tests/test_annotations.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 063c7c72..ac3dc31a 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -23,6 +23,8 @@ _obj_setattr = object.__setattr__ _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): diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 642d5cfd..f56424cf 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -11,6 +11,7 @@ import pytest import attr +from attr._make import _classvar_prefixes from attr.exceptions import UnannotatedAttributeError @@ -204,13 +205,14 @@ class TestAnnotations: 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