brain_attrs.py 3.5 KB

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