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

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"""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[ 

81 str, dict[nodes.If | nodes.IfExp, set[constraint.Constraint]] 

82 ] = {} 

83 """The constraints on nodes.""" 

84 

85 @property 

86 def nodes_inferred(self) -> int: 

87 """ 

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

89 

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

91 variable in the presence of __slots__ 

92 """ 

93 return self._nodes_inferred[0] 

94 

95 @nodes_inferred.setter 

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

97 self._nodes_inferred[0] = value 

98 

99 @property 

100 def inferred(self) -> _InferenceCache: 

101 """ 

102 Inferred node contexts to their mapped results. 

103 

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

105 and the value is tuple of the inferred results 

106 """ 

107 return _INFERENCE_CACHE 

108 

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

110 """Push node into inference path. 

111 

112 Allows one to see if the given node has already 

113 been looked at for this inference context 

114 """ 

115 name = self.lookupname 

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

117 return True 

118 

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

120 return False 

121 

122 def clone(self) -> InferenceContext: 

123 """Clone inference path. 

124 

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

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

127 so the InferenceContext will need be cloned 

128 """ 

129 # XXX copy lookupname/callcontext ? 

130 clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred) 

131 clone.callcontext = self.callcontext 

132 clone.boundnode = self.boundnode 

133 clone.extra_context = self.extra_context 

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

135 return clone 

136 

137 @contextlib.contextmanager 

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

139 path = set(self.path) 

140 yield 

141 self.path = path 

142 

143 def is_empty(self) -> bool: 

144 return ( 

145 not self.path 

146 and not self.nodes_inferred 

147 and not self.callcontext 

148 and not self.boundnode 

149 and not self.lookupname 

150 and not self.callcontext 

151 and not self.extra_context 

152 and not self.constraints 

153 ) 

154 

155 def __str__(self) -> str: 

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

157 

158 state = ( 

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

160 for field in self.__slots__ 

161 ) 

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

163 

164 

165class CallContext: 

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

167 

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

169 

170 def __init__( 

171 self, 

172 args: list[nodes.NodeNG], 

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

174 callee: InferenceResult | None = None, 

175 ): 

176 self.args = args # Call positional arguments 

177 if keywords: 

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

179 else: 

180 arg_value_pairs = [] 

181 self.keywords = arg_value_pairs # Call keyword arguments 

182 self.callee = callee # Function being called 

183 

184 

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

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

187 if context is not None: 

188 return context.clone() 

189 

190 return InferenceContext() 

191 

192 

193def bind_context_to_node( 

194 context: InferenceContext | None, node: SuccessfulInferenceResult 

195) -> InferenceContext: 

196 """Give a context a boundnode 

197 to retrieve the correct function name or attribute value 

198 with from further inference. 

199 

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

201 be incorrectly propagated higher up in the call stack. 

202 """ 

203 context = copy_context(context) 

204 context.boundnode = node 

205 return context