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"""
11
12from astroid import nodes
13from astroid.brain.helpers import is_class_var
14from astroid.manager import AstroidManager
15from astroid.util import safe_infer
16
17ATTRIB_NAMES = frozenset(
18 (
19 "attr.Factory",
20 "attr.ib",
21 "attrib",
22 "attr.attrib",
23 "attr.field",
24 "attrs.field",
25 "field",
26 )
27)
28NEW_ATTRS_NAMES = frozenset(
29 (
30 "attrs.define",
31 "attrs.mutable",
32 "attrs.frozen",
33 )
34)
35ATTRS_NAMES = frozenset(
36 (
37 "attr.s",
38 "attrs",
39 "attr.attrs",
40 "attr.attributes",
41 "attr.define",
42 "attr.mutable",
43 "attr.frozen",
44 *NEW_ATTRS_NAMES,
45 )
46)
47
48
49def is_decorated_with_attrs(node, decorator_names=ATTRS_NAMES) -> bool:
50 """Return whether a decorated node has an attr decorator applied."""
51 if not node.decorators:
52 return False
53 for decorator_attribute in node.decorators.nodes:
54 if isinstance(decorator_attribute, nodes.Call): # decorator with arguments
55 decorator_attribute = decorator_attribute.func
56 if decorator_attribute.as_string() in decorator_names:
57 return True
58
59 inferred = safe_infer(decorator_attribute)
60 if inferred and inferred.root().name == "attr._next_gen":
61 return True
62 return False
63
64
65def attr_attributes_transform(node: nodes.ClassDef) -> None:
66 """Given that the ClassNode has an attr decorator,
67 rewrite class attributes as instance attributes
68 """
69 # Astroid can't infer this attribute properly
70 # Prevents https://github.com/pylint-dev/pylint/issues/1884
71 node.locals["__attrs_attrs__"] = [nodes.Unknown(parent=node)]
72
73 use_bare_annotations = is_decorated_with_attrs(node, NEW_ATTRS_NAMES)
74 for cdef_body_node in node.body:
75 if not isinstance(cdef_body_node, (nodes.Assign, nodes.AnnAssign)):
76 continue
77 if isinstance(cdef_body_node.value, nodes.Call):
78 if cdef_body_node.value.func.as_string() not in ATTRIB_NAMES:
79 continue
80 elif not use_bare_annotations:
81 continue
82
83 # Skip attributes that are explicitly annotated as class variables
84 if isinstance(cdef_body_node, nodes.AnnAssign) and is_class_var(
85 cdef_body_node.annotation
86 ):
87 continue
88
89 targets = (
90 cdef_body_node.targets
91 if hasattr(cdef_body_node, "targets")
92 else [cdef_body_node.target]
93 )
94 for target in targets:
95 rhs_node = nodes.Unknown(
96 lineno=cdef_body_node.lineno,
97 col_offset=cdef_body_node.col_offset,
98 parent=cdef_body_node,
99 )
100 if isinstance(target, nodes.AssignName):
101 # Could be a subscript if the code analysed is
102 # i = Optional[str] = ""
103 # See https://github.com/pylint-dev/pylint/issues/4439
104 node.locals[target.name] = [rhs_node]
105 node.instance_attrs[target.name] = [rhs_node]
106
107
108def register(manager: AstroidManager) -> None:
109 manager.register_transform(
110 nodes.ClassDef, attr_attributes_transform, is_decorated_with_attrs
111 )