brain_functools.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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. """Astroid hooks for understanding functools library module."""
  5. from __future__ import annotations
  6. from collections.abc import Iterator
  7. from functools import partial
  8. from itertools import chain
  9. from astroid import BoundMethod, arguments, extract_node, helpers, nodes, objects
  10. from astroid.context import InferenceContext
  11. from astroid.exceptions import InferenceError, UseInferenceDefault
  12. from astroid.inference_tip import inference_tip
  13. from astroid.interpreter import objectmodel
  14. from astroid.manager import AstroidManager
  15. from astroid.nodes.node_classes import AssignName, Attribute, Call, Name
  16. from astroid.nodes.scoped_nodes import FunctionDef
  17. from astroid.util import UninferableBase
  18. LRU_CACHE = "functools.lru_cache"
  19. class LruWrappedModel(objectmodel.FunctionModel):
  20. """Special attribute model for functions decorated with functools.lru_cache.
  21. The said decorators patches at decoration time some functions onto
  22. the decorated function.
  23. """
  24. @property
  25. def attr___wrapped__(self):
  26. return self._instance
  27. @property
  28. def attr_cache_info(self):
  29. cache_info = extract_node(
  30. """
  31. from functools import _CacheInfo
  32. _CacheInfo(0, 0, 0, 0)
  33. """
  34. )
  35. class CacheInfoBoundMethod(BoundMethod):
  36. def infer_call_result(
  37. self, caller, context: InferenceContext | None = None
  38. ):
  39. yield helpers.safe_infer(cache_info)
  40. return CacheInfoBoundMethod(proxy=self._instance, bound=self._instance)
  41. @property
  42. def attr_cache_clear(self):
  43. node = extract_node("""def cache_clear(self): pass""")
  44. return BoundMethod(proxy=node, bound=self._instance.parent.scope())
  45. def _transform_lru_cache(node, context: InferenceContext | None = None) -> None:
  46. # TODO: this is not ideal, since the node should be immutable,
  47. # but due to https://github.com/PyCQA/astroid/issues/354,
  48. # there's not much we can do now.
  49. # Replacing the node would work partially, because,
  50. # in pylint, the old node would still be available, leading
  51. # to spurious false positives.
  52. node.special_attributes = LruWrappedModel()(node)
  53. def _functools_partial_inference(
  54. node: nodes.Call, context: InferenceContext | None = None
  55. ) -> Iterator[objects.PartialFunction]:
  56. call = arguments.CallSite.from_call(node, context=context)
  57. number_of_positional = len(call.positional_arguments)
  58. if number_of_positional < 1:
  59. raise UseInferenceDefault("functools.partial takes at least one argument")
  60. if number_of_positional == 1 and not call.keyword_arguments:
  61. raise UseInferenceDefault(
  62. "functools.partial needs at least to have some filled arguments"
  63. )
  64. partial_function = call.positional_arguments[0]
  65. try:
  66. inferred_wrapped_function = next(partial_function.infer(context=context))
  67. except (InferenceError, StopIteration) as exc:
  68. raise UseInferenceDefault from exc
  69. if isinstance(inferred_wrapped_function, UninferableBase):
  70. raise UseInferenceDefault("Cannot infer the wrapped function")
  71. if not isinstance(inferred_wrapped_function, FunctionDef):
  72. raise UseInferenceDefault("The wrapped function is not a function")
  73. # Determine if the passed keywords into the callsite are supported
  74. # by the wrapped function.
  75. if not inferred_wrapped_function.args:
  76. function_parameters = []
  77. else:
  78. function_parameters = chain(
  79. inferred_wrapped_function.args.args or (),
  80. inferred_wrapped_function.args.posonlyargs or (),
  81. inferred_wrapped_function.args.kwonlyargs or (),
  82. )
  83. parameter_names = {
  84. param.name for param in function_parameters if isinstance(param, AssignName)
  85. }
  86. if set(call.keyword_arguments) - parameter_names:
  87. raise UseInferenceDefault("wrapped function received unknown parameters")
  88. partial_function = objects.PartialFunction(
  89. call,
  90. name=inferred_wrapped_function.name,
  91. lineno=inferred_wrapped_function.lineno,
  92. col_offset=inferred_wrapped_function.col_offset,
  93. parent=node.parent,
  94. )
  95. partial_function.postinit(
  96. args=inferred_wrapped_function.args,
  97. body=inferred_wrapped_function.body,
  98. decorators=inferred_wrapped_function.decorators,
  99. returns=inferred_wrapped_function.returns,
  100. type_comment_returns=inferred_wrapped_function.type_comment_returns,
  101. type_comment_args=inferred_wrapped_function.type_comment_args,
  102. doc_node=inferred_wrapped_function.doc_node,
  103. )
  104. return iter((partial_function,))
  105. def _looks_like_lru_cache(node) -> bool:
  106. """Check if the given function node is decorated with lru_cache."""
  107. if not node.decorators:
  108. return False
  109. for decorator in node.decorators.nodes:
  110. if not isinstance(decorator, (Attribute, Call)):
  111. continue
  112. if _looks_like_functools_member(decorator, "lru_cache"):
  113. return True
  114. return False
  115. def _looks_like_functools_member(node: Attribute | Call, member: str) -> bool:
  116. """Check if the given Call node is the wanted member of functools."""
  117. if isinstance(node, Attribute):
  118. return node.attrname == member
  119. if isinstance(node.func, Name):
  120. return node.func.name == member
  121. if isinstance(node.func, Attribute):
  122. return (
  123. node.func.attrname == member
  124. and isinstance(node.func.expr, Name)
  125. and node.func.expr.name == "functools"
  126. )
  127. return False
  128. _looks_like_partial = partial(_looks_like_functools_member, member="partial")
  129. AstroidManager().register_transform(
  130. FunctionDef, _transform_lru_cache, _looks_like_lru_cache
  131. )
  132. AstroidManager().register_transform(
  133. Call,
  134. inference_tip(_functools_partial_inference),
  135. _looks_like_partial,
  136. )