Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/astroid/brain/brain_functools.py: 77%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

82 statements  

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 hooks for understanding functools library module.""" 

6 

7from __future__ import annotations 

8 

9from collections.abc import Iterator 

10from functools import partial 

11from itertools import chain 

12 

13from astroid import BoundMethod, arguments, nodes, objects 

14from astroid.builder import extract_node 

15from astroid.context import InferenceContext 

16from astroid.exceptions import InferenceError, UseInferenceDefault 

17from astroid.inference_tip import inference_tip 

18from astroid.interpreter import objectmodel 

19from astroid.manager import AstroidManager 

20from astroid.nodes.node_classes import AssignName, Attribute, Call, Name 

21from astroid.nodes.scoped_nodes import FunctionDef 

22from astroid.typing import InferenceResult, SuccessfulInferenceResult 

23from astroid.util import UninferableBase, safe_infer 

24 

25LRU_CACHE = "functools.lru_cache" 

26 

27 

28class LruWrappedModel(objectmodel.FunctionModel): 

29 """Special attribute model for functions decorated with functools.lru_cache. 

30 

31 The said decorators patches at decoration time some functions onto 

32 the decorated function. 

33 """ 

34 

35 @property 

36 def attr___wrapped__(self): 

37 return self._instance 

38 

39 @property 

40 def attr_cache_info(self): 

41 cache_info = extract_node( 

42 """ 

43 from functools import _CacheInfo 

44 _CacheInfo(0, 0, 0, 0) 

45 """ 

46 ) 

47 

48 class CacheInfoBoundMethod(BoundMethod): 

49 def infer_call_result( 

50 self, 

51 caller: SuccessfulInferenceResult | None, 

52 context: InferenceContext | None = None, 

53 ) -> Iterator[InferenceResult]: 

54 res = safe_infer(cache_info) 

55 assert res is not None 

56 yield res 

57 

58 return CacheInfoBoundMethod(proxy=self._instance, bound=self._instance) 

59 

60 @property 

61 def attr_cache_clear(self): 

62 node = extract_node("""def cache_clear(self): pass""") 

63 return BoundMethod(proxy=node, bound=self._instance.parent.scope()) 

64 

65 

66def _transform_lru_cache(node, context: InferenceContext | None = None) -> None: 

67 # TODO: this is not ideal, since the node should be immutable, 

68 # but due to https://github.com/pylint-dev/astroid/issues/354, 

69 # there's not much we can do now. 

70 # Replacing the node would work partially, because, 

71 # in pylint, the old node would still be available, leading 

72 # to spurious false positives. 

73 node.special_attributes = LruWrappedModel()(node) 

74 

75 

76def _functools_partial_inference( 

77 node: nodes.Call, context: InferenceContext | None = None 

78) -> Iterator[objects.PartialFunction]: 

79 call = arguments.CallSite.from_call(node, context=context) 

80 number_of_positional = len(call.positional_arguments) 

81 if number_of_positional < 1: 

82 raise UseInferenceDefault("functools.partial takes at least one argument") 

83 if number_of_positional == 1 and not call.keyword_arguments: 

84 raise UseInferenceDefault( 

85 "functools.partial needs at least to have some filled arguments" 

86 ) 

87 

88 partial_function = call.positional_arguments[0] 

89 try: 

90 inferred_wrapped_function = next(partial_function.infer(context=context)) 

91 except (InferenceError, StopIteration) as exc: 

92 raise UseInferenceDefault from exc 

93 if isinstance(inferred_wrapped_function, UninferableBase): 

94 raise UseInferenceDefault("Cannot infer the wrapped function") 

95 if not isinstance(inferred_wrapped_function, FunctionDef): 

96 raise UseInferenceDefault("The wrapped function is not a function") 

97 

98 # Determine if the passed keywords into the callsite are supported 

99 # by the wrapped function. 

100 if not inferred_wrapped_function.args: 

101 function_parameters = [] 

102 else: 

103 function_parameters = chain( 

104 inferred_wrapped_function.args.args or (), 

105 inferred_wrapped_function.args.posonlyargs or (), 

106 inferred_wrapped_function.args.kwonlyargs or (), 

107 ) 

108 parameter_names = { 

109 param.name for param in function_parameters if isinstance(param, AssignName) 

110 } 

111 if set(call.keyword_arguments) - parameter_names: 

112 raise UseInferenceDefault("wrapped function received unknown parameters") 

113 

114 partial_function = objects.PartialFunction( 

115 call, 

116 name=inferred_wrapped_function.name, 

117 lineno=inferred_wrapped_function.lineno, 

118 col_offset=inferred_wrapped_function.col_offset, 

119 parent=node.parent, 

120 ) 

121 partial_function.postinit( 

122 args=inferred_wrapped_function.args, 

123 body=inferred_wrapped_function.body, 

124 decorators=inferred_wrapped_function.decorators, 

125 returns=inferred_wrapped_function.returns, 

126 type_comment_returns=inferred_wrapped_function.type_comment_returns, 

127 type_comment_args=inferred_wrapped_function.type_comment_args, 

128 doc_node=inferred_wrapped_function.doc_node, 

129 ) 

130 return iter((partial_function,)) 

131 

132 

133def _looks_like_lru_cache(node) -> bool: 

134 """Check if the given function node is decorated with lru_cache.""" 

135 if not node.decorators: 

136 return False 

137 for decorator in node.decorators.nodes: 

138 if not isinstance(decorator, (Attribute, Call)): 

139 continue 

140 if _looks_like_functools_member(decorator, "lru_cache"): 

141 return True 

142 return False 

143 

144 

145def _looks_like_functools_member(node: Attribute | Call, member: str) -> bool: 

146 """Check if the given Call node is the wanted member of functools.""" 

147 if isinstance(node, Attribute): 

148 return node.attrname == member 

149 if isinstance(node.func, Name): 

150 return node.func.name == member 

151 if isinstance(node.func, Attribute): 

152 return ( 

153 node.func.attrname == member 

154 and isinstance(node.func.expr, Name) 

155 and node.func.expr.name == "functools" 

156 ) 

157 return False 

158 

159 

160_looks_like_partial = partial(_looks_like_functools_member, member="partial") 

161 

162 

163def register(manager: AstroidManager) -> None: 

164 manager.register_transform(FunctionDef, _transform_lru_cache, _looks_like_lru_cache) 

165 

166 manager.register_transform( 

167 Call, 

168 inference_tip(_functools_partial_inference), 

169 _looks_like_partial, 

170 )