Skip to content

Commit 5eae215

Browse files
authored
brain_attrs: Support annotation-only members (#2515)
Similar to dataclasses, the following class which uses instance variable annotations is valid: ```py @attrs.define class AttrsCls: x: int AttrsCls(1).x ``` However, before this commit astroid failed to transform the class attribute into an instance attribute and this led to `no-member` errors in pylint. Only the new `attrs` API supports this form out-of-the-box, so just address the common case. Closes #2514
1 parent 4ae4617 commit 5eae215

File tree

4 files changed

+47
-5
lines changed

4 files changed

+47
-5
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ What's New in astroid 3.3.3?
1313
============================
1414
Release date: TBA
1515

16+
* Add annotation-only instance attributes to attrs classes to fix `no-member` false positives.
17+
18+
Closes #2514
19+
1620

1721

1822
What's New in astroid 3.3.2?

astroid/brain/brain_attrs.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
"field",
2525
)
2626
)
27+
NEW_ATTRS_NAMES = frozenset(
28+
(
29+
"attrs.define",
30+
"attrs.mutable",
31+
"attrs.frozen",
32+
)
33+
)
2734
ATTRS_NAMES = frozenset(
2835
(
2936
"attr.s",
@@ -33,9 +40,7 @@
3340
"attr.define",
3441
"attr.mutable",
3542
"attr.frozen",
36-
"attrs.define",
37-
"attrs.mutable",
38-
"attrs.frozen",
43+
*NEW_ATTRS_NAMES,
3944
)
4045
)
4146

@@ -64,13 +69,14 @@ def attr_attributes_transform(node: ClassDef) -> None:
6469
# Prevents https://github.com/pylint-dev/pylint/issues/1884
6570
node.locals["__attrs_attrs__"] = [Unknown(parent=node)]
6671

72+
use_bare_annotations = is_decorated_with_attrs(node, NEW_ATTRS_NAMES)
6773
for cdef_body_node in node.body:
6874
if not isinstance(cdef_body_node, (Assign, AnnAssign)):
6975
continue
7076
if isinstance(cdef_body_node.value, Call):
7177
if cdef_body_node.value.func.as_string() not in ATTRIB_NAMES:
7278
continue
73-
else:
79+
elif not use_bare_annotations:
7480
continue
7581
targets = (
7682
cdef_body_node.targets

script/.contributors_aliases.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@
8585
"name": "Hippo91",
8686
"team": "Maintainers"
8787
},
88+
89+
90+
"name": "Hashem Nasarat"
91+
},
8892
8993
"mails": ["[email protected]"],
9094
"name": "Hugo van Kemenade"

tests/brain/test_attr.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import unittest
88

99
import astroid
10-
from astroid import nodes
10+
from astroid import exceptions, nodes
1111

1212
try:
1313
import attr # type: ignore[import] # pylint: disable=unused-import
@@ -201,3 +201,31 @@ class Foo:
201201
"""
202202
should_be_unknown = next(astroid.extract_node(code).infer()).getattr("bar")[0]
203203
self.assertIsInstance(should_be_unknown, astroid.Unknown)
204+
205+
def test_attr_with_only_annotation_fails(self) -> None:
206+
code = """
207+
import attr
208+
209+
@attr.s
210+
class Foo:
211+
bar: int
212+
Foo()
213+
"""
214+
with self.assertRaises(exceptions.AttributeInferenceError):
215+
next(astroid.extract_node(code).infer()).getattr("bar")
216+
217+
def test_attrs_with_only_annotation_works(self) -> None:
218+
code = """
219+
import attrs
220+
221+
@attrs.define
222+
class Foo:
223+
bar: int
224+
baz: str = "hello"
225+
Foo(1)
226+
"""
227+
for attr_name in ("bar", "baz"):
228+
should_be_unknown = next(astroid.extract_node(code).infer()).getattr(
229+
attr_name
230+
)[0]
231+
self.assertIsInstance(should_be_unknown, astroid.Unknown)

0 commit comments

Comments
 (0)