brain_attrs.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  2. # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
  3. """
  4. Astroid hook for the attrs library
  5. Without this hook pylint reports unsupported-assignment-operation
  6. for attrs classes
  7. """
  8. from astroid.manager import AstroidManager
  9. from astroid.nodes.node_classes import AnnAssign, Assign, AssignName, Call, Unknown
  10. from astroid.nodes.scoped_nodes import ClassDef
  11. ATTRIB_NAMES = frozenset(("attr.ib", "attrib", "attr.attrib", "attr.field", "field"))
  12. ATTRS_NAMES = frozenset(
  13. (
  14. "attr.s",
  15. "attrs",
  16. "attr.attrs",
  17. "attr.attributes",
  18. "attr.define",
  19. "attr.mutable",
  20. "attr.frozen",
  21. )
  22. )
  23. def is_decorated_with_attrs(node, decorator_names=ATTRS_NAMES):
  24. """Return True if a decorated node has
  25. an attr decorator applied."""
  26. if not node.decorators:
  27. return False
  28. for decorator_attribute in node.decorators.nodes:
  29. if isinstance(decorator_attribute, Call): # decorator with arguments
  30. decorator_attribute = decorator_attribute.func
  31. if decorator_attribute.as_string() in decorator_names:
  32. return True
  33. return False
  34. def attr_attributes_transform(node: ClassDef) -> None:
  35. """Given that the ClassNode has an attr decorator,
  36. rewrite class attributes as instance attributes
  37. """
  38. # Astroid can't infer this attribute properly
  39. # Prevents https://github.com/PyCQA/pylint/issues/1884
  40. node.locals["__attrs_attrs__"] = [Unknown(parent=node)]
  41. for cdef_body_node in node.body:
  42. if not isinstance(cdef_body_node, (Assign, AnnAssign)):
  43. continue
  44. if isinstance(cdef_body_node.value, Call):
  45. if cdef_body_node.value.func.as_string() not in ATTRIB_NAMES:
  46. continue
  47. else:
  48. continue
  49. targets = (
  50. cdef_body_node.targets
  51. if hasattr(cdef_body_node, "targets")
  52. else [cdef_body_node.target]
  53. )
  54. for target in targets:
  55. rhs_node = Unknown(
  56. lineno=cdef_body_node.lineno,
  57. col_offset=cdef_body_node.col_offset,
  58. parent=cdef_body_node,
  59. )
  60. if isinstance(target, AssignName):
  61. # Could be a subscript if the code analysed is
  62. # i = Optional[str] = ""
  63. # See https://github.com/PyCQA/pylint/issues/4439
  64. node.locals[target.name] = [rhs_node]
  65. node.instance_attrs[target.name] = [rhs_node]
  66. AstroidManager().register_transform(
  67. ClassDef, attr_attributes_transform, is_decorated_with_attrs
  68. )