diff --git a/changelog.d/1593.change.md b/changelog.d/1593.change.md new file mode 100644 index 000000000..d3fd8eb43 --- /dev/null +++ b/changelog.d/1593.change.md @@ -0,0 +1 @@ +Unimported `typing.ClassVar` annotations created as forward references are now detected as class variables. diff --git a/src/attr/_make.py b/src/attr/_make.py index 6794464e9..1b447fac4 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -299,6 +299,7 @@ def _is_class_var(annot): annotations which would put attrs-based classes at a performance disadvantage compared to plain old classes. """ + annot = getattr(annot, "__forward_arg__", annot) annot = str(annot) # Annotation can be quoted. diff --git a/tests/test_annotations.py b/tests/test_annotations.py index e193d4ba1..de3d10589 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -434,6 +434,23 @@ class C: assert_init_annotations(C) + @pytest.mark.skipif( + sys.version_info[:2] < (3, 14), + reason="Python 3.14 changed annotation evaluation behavior.", + ) + def test_missing_classvar_import(self, slots): + """ + Unimported ClassVars are recognized as ForwardRefs. + """ + + @attr.s(auto_attribs=True, slots=slots) + class C: + cls_var: ClassVar[str] # noqa: F821 + value: int = 1 + + assert "cls_var" not in attr.fields_dict(C) + assert 1 == C().value + def test_keyword_only_auto_attribs(self): """ `kw_only` propagates to attributes defined via `auto_attribs`. @@ -691,6 +708,7 @@ class A: "typing.ClassVar", "'typing.ClassVar[dict]'", "t.ClassVar[int]", + typing.ForwardRef("ClassVar[str]"), ], ) def test_is_class_var(annot):