brain_functools.py 6.3 KB

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