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

77 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:53 +0000

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 

10import pprint 

11from collections.abc import Iterator 

12from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple 

13 

14from astroid.typing import InferenceResult, SuccessfulInferenceResult 

15 

16if TYPE_CHECKING: 

17 from astroid import constraint, nodes 

18 from astroid.nodes.node_classes import Keyword, NodeNG 

19 

20_InferenceCache = Dict[ 

21 Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"] 

22] 

23 

24_INFERENCE_CACHE: _InferenceCache = {} 

25 

26 

27def _invalidate_cache() -> None: 

28 _INFERENCE_CACHE.clear() 

29 

30 

31class InferenceContext: 

32 """Provide context for inference. 

33 

34 Store already inferred nodes to save time 

35 Account for already visited nodes to stop infinite recursion 

36 """ 

37 

38 __slots__ = ( 

39 "path", 

40 "lookupname", 

41 "callcontext", 

42 "boundnode", 

43 "extra_context", 

44 "constraints", 

45 "_nodes_inferred", 

46 ) 

47 

48 max_inferred = 100 

49 

50 def __init__( 

51 self, 

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

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

54 ) -> None: 

55 if nodes_inferred is None: 

56 self._nodes_inferred = [0] 

57 else: 

58 self._nodes_inferred = nodes_inferred 

59 

60 self.path = path or set() 

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

62 

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

64 """ 

65 self.lookupname: str | None = None 

66 """The original name of the node. 

67 

68 e.g. 

69 foo = 1 

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

71 """ 

72 self.callcontext: CallContext | None = None 

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

74 self.boundnode: SuccessfulInferenceResult | None = None 

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

76 

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

78 """ 

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

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

81 

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

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 state = ( 

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

158 for field in self.__slots__ 

159 ) 

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

161 

162 

163class CallContext: 

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

165 

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

167 

168 def __init__( 

169 self, 

170 args: list[NodeNG], 

171 keywords: list[Keyword] | None = None, 

172 callee: InferenceResult | None = None, 

173 ): 

174 self.args = args # Call positional arguments 

175 if keywords: 

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

177 else: 

178 arg_value_pairs = [] 

179 self.keywords = arg_value_pairs # Call keyword arguments 

180 self.callee = callee # Function being called 

181 

182 

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

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

185 if context is not None: 

186 return context.clone() 

187 

188 return InferenceContext() 

189 

190 

191def bind_context_to_node( 

192 context: InferenceContext | None, node: SuccessfulInferenceResult 

193) -> InferenceContext: 

194 """Give a context a boundnode 

195 to retrieve the correct function name or attribute value 

196 with from further inference. 

197 

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

199 be incorrectly propagated higher up in the call stack. 

200 """ 

201 context = copy_context(context) 

202 context.boundnode = node 

203 return context