brain_attrs.py 3.0 KB

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