Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/context.py: 92%

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

85 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"""Various context related utilities, including inference and call contexts.""" 

6 

7from __future__ import annotations 

8 

9import contextlib 

10from collections.abc import Iterator, Sequence 

11from typing import TYPE_CHECKING 

12 

13from astroid.typing import InferenceResult, SuccessfulInferenceResult 

14 

15if TYPE_CHECKING: 

16 from astroid import constraint, nodes 

17 

18_InferenceCache = dict[ 

19 tuple["nodes.NodeNG", str | None, str | None, str | None], Sequence["nodes.NodeNG"] 

20] 

21 

22_INFERENCE_CACHE: _InferenceCache = {} 

23 

24 

25def _invalidate_cache() -> None: 

26 _INFERENCE_CACHE.clear() 

27 

28 

29class InferenceContext: 

30 """Provide context for inference. 

31 

32 Store already inferred nodes to save time 

33 Account for already visited nodes to stop infinite recursion 

34 """ 

35 

36 __slots__ = ( 

37 "_nodes_inferred", 

38 "boundnode", 

39 "callcontext", 

40 "constraints", 

41 "extra_context", 

42 "lookupname", 

43 "path", 

44 ) 

45 

46 max_inferred = 100 

47 

48 def __init__( 

49 self, 

50 path: set[tuple[nodes.NodeNG, str | None]] | None = None, 

51 nodes_inferred: list[int] | None = None, 

52 ) -> None: 

53 if nodes_inferred is None: 

54 self._nodes_inferred = [0] 

55 else: 

56 self._nodes_inferred = nodes_inferred 

57 

58 self.path = path or set() 

59 """Path of visited nodes and their lookupname. 

60 

61 Currently this key is ``(node, context.lookupname)`` 

62 """ 

63 self.lookupname: str | None = None 

64 """The original name of the node. 

65 

66 e.g. 

67 foo = 1 

68 The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo' 

69 """ 

70 self.callcontext: CallContext | None = None 

71 """The call arguments and keywords for the given context.""" 

72 self.boundnode: SuccessfulInferenceResult | None = None 

73 """The bound node of the given context. 

74 

75 e.g. the bound node of object.__new__(cls) is the object node 

76 """ 

77 self.extra_context: dict[SuccessfulInferenceResult, InferenceContext] = {} 

78 """Context that needs to be passed down through call stacks for call arguments.""" 

79 

80 self.constraints: dict[str, dict[nodes.NodeNG, set[constraint.Constraint]]] = {} 

81 """The constraints on nodes.""" 

82 

83 @property 

84 def nodes_inferred(self) -> int: 

85 """ 

86 Number of nodes inferred in this context and all its clones/descendents. 

87 

88 Wrap inner value in a mutable cell to allow for mutating a class 

89 variable in the presence of __slots__ 

90 """ 

91 return self._nodes_inferred[0] 

92 

93 @nodes_inferred.setter 

94 def nodes_inferred(self, value: int) -> None: 

95 self._nodes_inferred[0] = value 

96 

97 @property 

98 def inferred(self) -> _InferenceCache: 

99 """ 

100 Inferred node contexts to their mapped results. 

101 

102 Currently the key is ``(node, lookupname, callcontext, boundnode)`` 

103 and the value is tuple of the inferred results 

104 """ 

105 return _INFERENCE_CACHE 

106 

107 def push(self, node: nodes.NodeNG) -> bool: 

108 """Push node into inference path. 

109 

110 Allows one to see if the given node has already 

111 been looked at for this inference context 

112 """ 

113 name = self.lookupname 

114 if (node, name) in self.path: 

115 return True 

116 

117 self.path.add((node, name)) 

118 return False 

119 

120 def clone(self) -> InferenceContext: 

121 """Clone inference path. 

122 

123 For example, each side of a binary operation (BinOp) 

124 starts with the same context but diverge as each side is inferred 

125 so the InferenceContext will need be cloned 

126 """ 

127 # XXX copy lookupname/callcontext ? 

128 # Bypass __init__: with __slots__ all attributes are written directly 

129 # below, so the conditionals/defaults in __init__ would be wasted work 

130 # on what is the hottest constructor in inference (~85k calls per 

131 # pandas/frame run, see #1115). 

132 clone = InferenceContext.__new__(InferenceContext) 

133 clone._nodes_inferred = self._nodes_inferred 

134 clone.path = self.path.copy() 

135 clone.lookupname = None 

136 clone.callcontext = self.callcontext 

137 clone.boundnode = self.boundnode 

138 clone.extra_context = self.extra_context 

139 clone.constraints = self.constraints.copy() 

140 return clone 

141 

142 @contextlib.contextmanager 

143 def restore_path(self) -> Iterator[None]: 

144 path = set(self.path) 

145 yield 

146 self.path = path 

147 

148 def is_empty(self) -> bool: 

149 return ( 

150 not self.path 

151 and not self.nodes_inferred 

152 and not self.callcontext 

153 and not self.boundnode 

154 and not self.lookupname 

155 and not self.callcontext 

156 and not self.extra_context 

157 and not self.constraints 

158 ) 

159 

160 def __str__(self) -> str: 

161 import pprint # pylint: disable=import-outside-toplevel 

162 

163 state = ( 

164 f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}" 

165 for field in self.__slots__ 

166 ) 

167 return "{}({})".format(type(self).__name__, ",\n ".join(state)) 

168 

169 

170class CallContext: 

171 """Holds information for a call site.""" 

172 

173 __slots__ = ("args", "callee", "keywords") 

174 

175 def __init__( 

176 self, 

177 args: list[nodes.NodeNG], 

178 keywords: list[nodes.Keyword] | None = None, 

179 callee: InferenceResult | None = None, 

180 ): 

181 self.args = args # Call positional arguments 

182 if keywords: 

183 arg_value_pairs = [(arg.arg, arg.value) for arg in keywords] 

184 else: 

185 arg_value_pairs = [] 

186 self.keywords = arg_value_pairs # Call keyword arguments 

187 self.callee = callee # Function being called 

188 

189 

190def copy_context(context: InferenceContext | None) -> InferenceContext: 

191 """Clone a context if given, or return a fresh context.""" 

192 if context is not None: 

193 return context.clone() 

194 

195 return InferenceContext() 

196 

197 

198def bind_context_to_node( 

199 context: InferenceContext | None, node: SuccessfulInferenceResult 

200) -> InferenceContext: 

201 """Give a context a boundnode 

202 to retrieve the correct function name or attribute value 

203 with from further inference. 

204 

205 Do not use an existing context since the boundnode could then 

206 be incorrectly propagated higher up in the call stack. 

207 """ 

208 context = copy_context(context) 

209 context.boundnode = node 

210 return context