1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
4
5"""
6Astroid hook for the attrs library
7
8Without this hook pylint reports unsupported-assignment-operation
9for attrs classes
10"""
11from astroid.manager import AstroidManager
12from astroid.nodes.node_classes import AnnAssign, Assign, AssignName, Call, Unknown
13from astroid.nodes.scoped_nodes import ClassDef
14from astroid.util import safe_infer
15
16ATTRIB_NAMES = frozenset(
17 (
18 "attr.Factory",
19 "attr.ib",
20 "attrib",
21 "attr.attrib",
22 "attr.field",
23 "attrs.field",
24 "field",
25 )
26)
27NEW_ATTRS_NAMES = frozenset(
28 (
29 "attrs.define",
30 "attrs.mutable",
31 "attrs.frozen",
32 )
33)
34ATTRS_NAMES = frozenset(
35 (
36 "attr.s",
37 "attrs",
38 "attr.attrs",
39 "attr.attributes",
40 "attr.define",
41 "attr.mutable",
42 "attr.frozen",
43 *NEW_ATTRS_NAMES,
44 )
45)
46
47
48def is_decorated_with_attrs(node, decorator_names=ATTRS_NAMES) -> bool:
49 """Return whether a decorated node has an attr decorator applied."""
50 if not node.decorators:
51 return False
52 for decorator_attribute in node.decorators.nodes:
53 if isinstance(decorator_attribute, Call): # decorator with arguments
54 decorator_attribute = decorator_attribute.func
55 if decorator_attribute.as_string() in decorator_names:
56 return True
57
58 inferred = safe_infer(decorator_attribute)
59 if inferred and inferred.root().name == "attr._next_gen":
60 return True
61 return False
62
63
64def attr_attributes_transform(node: ClassDef) -> None:
65 """Given that the ClassNode has an attr decorator,
66 rewrite class attributes as instance attributes
67 """
68 # Astroid can't infer this attribute properly
69 # Prevents https://github.com/pylint-dev/pylint/issues/1884
70 node.locals["__attrs_attrs__"] = [Unknown(parent=node)]
71
72 use_bare_annotations = is_decorated_with_attrs(node, NEW_ATTRS_NAMES)
73 for cdef_body_node in node.body:
74 if not isinstance(cdef_body_node, (Assign, AnnAssign)):
75 continue
76 if isinstance(cdef_body_node.value, Call):
77 if cdef_body_node.value.func.as_string() not in ATTRIB_NAMES:
78 continue
79 elif not use_bare_annotations:
80 continue
81 targets = (
82 cdef_body_node.targets
83 if hasattr(cdef_body_node, "targets")
84 else [cdef_body_node.target]
85 )
86 for target in targets:
87 rhs_node = Unknown(
88 lineno=cdef_body_node.lineno,
89 col_offset=cdef_body_node.col_offset,
90 parent=cdef_body_node,
91 )
92 if isinstance(target, AssignName):
93 # Could be a subscript if the code analysed is
94 # i = Optional[str] = ""
95 # See https://github.com/pylint-dev/pylint/issues/4439
96 node.locals[target.name] = [rhs_node]
97 node.instance_attrs[target.name] = [rhs_node]
98
99
100def register(manager: AstroidManager) -> None:
101 manager.register_transform(
102 ClassDef, attr_attributes_transform, is_decorated_with_attrs
103 )