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

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

154 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"""Classes representing different types of constraints on inference values.""" 

6 

7from __future__ import annotations 

8 

9import sys 

10from abc import ABC, abstractmethod 

11from collections.abc import Iterator 

12from typing import TYPE_CHECKING 

13 

14from astroid import helpers, nodes, util 

15from astroid.context import InferenceContext 

16from astroid.exceptions import AstroidTypeError, InferenceError, MroError 

17from astroid.typing import InferenceResult 

18 

19if sys.version_info >= (3, 11): 

20 from typing import Self 

21else: 

22 from typing_extensions import Self 

23 

24if TYPE_CHECKING: 

25 from astroid import bases 

26 

27_NameNodes = nodes.AssignAttr | nodes.Attribute | nodes.AssignName | nodes.Name 

28 

29 

30class Constraint(ABC): 

31 """Represents a single constraint on a variable.""" 

32 

33 def __init__(self, node: nodes.NodeNG, negate: bool) -> None: 

34 self.node = node 

35 """The node that this constraint applies to.""" 

36 self.negate = negate 

37 """True if this constraint is negated. E.g., "is not" instead of "is".""" 

38 

39 @classmethod 

40 @abstractmethod 

41 def match( 

42 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False 

43 ) -> Self | None: 

44 """Return a new constraint for node matched from expr, if expr matches 

45 the constraint pattern. 

46 

47 If negate is True, negate the constraint. 

48 """ 

49 

50 @abstractmethod 

51 def satisfied_by( 

52 self, inferred: InferenceResult, context: InferenceContext 

53 ) -> bool: 

54 """Return True if this constraint is satisfied by the given inferred value.""" 

55 

56 

57class NoneConstraint(Constraint): 

58 """Represents an "is None" or "is not None" constraint.""" 

59 

60 CONST_NONE: nodes.Const = nodes.Const(None) 

61 

62 @classmethod 

63 def match( 

64 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False 

65 ) -> Self | None: 

66 """Return a new constraint for node matched from expr, if expr matches 

67 the constraint pattern. 

68 

69 Negate the constraint based on the value of negate. 

70 """ 

71 if isinstance(expr, nodes.Compare) and len(expr.ops) == 1: 

72 left = expr.left 

73 op, right = expr.ops[0] 

74 if op in {"is", "is not"} and ( 

75 _matches(left, node) and _matches(right, cls.CONST_NONE) 

76 ): 

77 negate = (op == "is" and negate) or (op == "is not" and not negate) 

78 return cls(node=node, negate=negate) 

79 

80 return None 

81 

82 def satisfied_by( 

83 self, inferred: InferenceResult, context: InferenceContext 

84 ) -> bool: 

85 """Return True if this constraint is satisfied by the given inferred value.""" 

86 # Assume true if uninferable 

87 if inferred is util.Uninferable: 

88 return True 

89 

90 # Return the XOR of self.negate and matches(inferred, self.CONST_NONE) 

91 return self.negate ^ _matches(inferred, self.CONST_NONE) 

92 

93 

94class BooleanConstraint(Constraint): 

95 """Represents an "x" or "not x" constraint.""" 

96 

97 @classmethod 

98 def match( 

99 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False 

100 ) -> Self | None: 

101 """Return a new constraint for node if expr matches one of these patterns: 

102 

103 - direct match (expr == node): use given negate value 

104 - negated match (expr == `not node`): flip negate value 

105 

106 Return None if no pattern matches. 

107 """ 

108 if _matches(expr, node): 

109 return cls(node=node, negate=negate) 

110 

111 if ( 

112 isinstance(expr, nodes.UnaryOp) 

113 and expr.op == "not" 

114 and _matches(expr.operand, node) 

115 ): 

116 return cls(node=node, negate=not negate) 

117 

118 return None 

119 

120 def satisfied_by( 

121 self, inferred: InferenceResult, context: InferenceContext 

122 ) -> bool: 

123 """Return True for uninferable results, or depending on negate flag: 

124 

125 - negate=False: satisfied if boolean value is True 

126 - negate=True: satisfied if boolean value is False 

127 """ 

128 inferred_booleaness = inferred.bool_value() 

129 if inferred is util.Uninferable or inferred_booleaness is util.Uninferable: 

130 return True 

131 

132 return self.negate ^ inferred_booleaness 

133 

134 

135class TypeConstraint(Constraint): 

136 """Represents an "isinstance(x, y)" constraint.""" 

137 

138 def __init__( 

139 self, node: nodes.NodeNG, classinfo: nodes.NodeNG, negate: bool 

140 ) -> None: 

141 super().__init__(node=node, negate=negate) 

142 self.classinfo = classinfo 

143 

144 @classmethod 

145 def match( 

146 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False 

147 ) -> Self | None: 

148 """Return a new constraint for node if expr matches the 

149 "isinstance(x, y)" pattern. Else, return None. 

150 """ 

151 is_instance_call = ( 

152 isinstance(expr, nodes.Call) 

153 and isinstance(expr.func, nodes.Name) 

154 and expr.func.name == "isinstance" 

155 and not expr.keywords 

156 and len(expr.args) == 2 

157 ) 

158 if is_instance_call and _matches(expr.args[0], node): 

159 return cls(node=node, classinfo=expr.args[1], negate=negate) 

160 

161 return None 

162 

163 def satisfied_by( 

164 self, inferred: InferenceResult, context: InferenceContext 

165 ) -> bool: 

166 """Return True for uninferable results, or depending on negate flag: 

167 

168 - negate=False: satisfied when inferred is an instance of the checked types. 

169 - negate=True: satisfied when inferred is not an instance of the checked types. 

170 """ 

171 if inferred is util.Uninferable: 

172 return True 

173 

174 # This method is called once per inferred value, with the same context. 

175 # Use a clone: inferring the classinfo pushes it onto the context's 

176 # inference path but only its first value is consumed, so nothing is 

177 # cached and a reused context would make the classinfo uninferable 

178 # from the second call on. 

179 context = context.clone() 

180 try: 

181 types = helpers.class_or_tuple_to_container(self.classinfo, context) 

182 matches_checked_types = helpers.object_isinstance(inferred, types, context) 

183 

184 if matches_checked_types is util.Uninferable: 

185 return True 

186 

187 return self.negate ^ matches_checked_types 

188 except (InferenceError, AstroidTypeError, MroError): 

189 return True 

190 

191 

192class EqualityConstraint(Constraint): 

193 """Represents a "==" or "!=" constraint.""" 

194 

195 def __init__(self, node: nodes.NodeNG, operand: nodes.NodeNG, negate: bool) -> None: 

196 super().__init__(node=node, negate=negate) 

197 self.operand = operand 

198 

199 @classmethod 

200 def match( 

201 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False 

202 ) -> Self | None: 

203 """Return a new constraint for node if expr matches one of these patterns: 

204 

205 - "node == operand" or "operand == node": use given negate value 

206 - "node != operand" or "operand != node": flip negate value 

207 

208 Return None if no pattern matches. 

209 """ 

210 if isinstance(expr, nodes.Compare) and len(expr.ops) == 1: 

211 left = expr.left 

212 op, right = expr.ops[0] 

213 matches_left = _matches(left, node) 

214 

215 if op in {"==", "!="} and (matches_left or _matches(right, node)): 

216 operand = right if matches_left else left 

217 negate = (op == "==" and negate) or (op == "!=" and not negate) 

218 return cls(node=node, operand=operand, negate=negate) 

219 

220 return None 

221 

222 def satisfied_by( 

223 self, inferred: InferenceResult, context: InferenceContext 

224 ) -> bool: 

225 """Return True for uninferable/ambiguous results, or depending on negate flag: 

226 

227 - negate=False: satisfied when both operands are equal. 

228 - negate=True: satisfied when both operands are not equal. 

229 

230 Only comparisons between constants and callables are supported. 

231 """ 

232 if inferred is util.Uninferable: 

233 return True 

234 

235 operand_inferred = util.safe_infer(self.operand, context) 

236 if operand_inferred is util.Uninferable or operand_inferred is None: 

237 return True 

238 

239 if isinstance(inferred, nodes.Const) and isinstance( 

240 operand_inferred, nodes.Const 

241 ): 

242 return self.negate ^ (inferred.value == operand_inferred.value) 

243 

244 if inferred.callable() and operand_inferred.callable(): 

245 return self.negate ^ (inferred is operand_inferred) 

246 

247 return True 

248 

249 

250def get_constraints( 

251 expr: _NameNodes, frame: nodes.LocalsDictNodeNG 

252) -> dict[nodes.NodeNG, set[Constraint]]: 

253 """Returns the constraints for the given expression. 

254 

255 The returned dictionary maps the node where the constraint was generated to the 

256 corresponding constraint(s). 

257 

258 Constraints are computed statically by analysing the code surrounding expr. 

259 Currently this only supports constraints generated from if conditions and 

260 comprehension conditions. 

261 """ 

262 current_node: nodes.NodeNG | None = expr 

263 constraints_mapping: dict[nodes.NodeNG, set[Constraint]] = {} 

264 while current_node is not None and current_node is not frame: 

265 parent = current_node.parent 

266 if isinstance(parent, (nodes.If, nodes.IfExp)): 

267 branch, _ = parent.locate_child(current_node) 

268 constraints: set[Constraint] | None = None 

269 if branch == "body": 

270 constraints = set(_match_constraint(expr, parent.test)) 

271 elif branch == "orelse": 

272 constraints = set(_match_constraint(expr, parent.test, invert=True)) 

273 

274 if constraints: 

275 constraints_mapping[parent] = constraints 

276 elif isinstance(parent, nodes.Comprehension): 

277 try: 

278 index = parent.ifs.index(current_node) 

279 except ValueError: 

280 pass 

281 else: 

282 # Preceding conditions of the same generator guard this condition. 

283 _add_ifs_constraints(expr, parent.ifs[:index], constraints_mapping) 

284 elif isinstance( 

285 parent, (nodes.ListComp, nodes.SetComp, nodes.DictComp, nodes.GeneratorExp) 

286 ): 

287 branch, _ = parent.locate_child(current_node) 

288 if branch == "generators": 

289 # Conditions guard the iterables of all later generators. 

290 index = parent.generators.index(current_node) 

291 generators = parent.generators[:index] 

292 else: # elt, key or value: guarded by all conditions 

293 generators = parent.generators 

294 for comprehension in generators: 

295 _add_ifs_constraints(expr, comprehension.ifs, constraints_mapping) 

296 current_node = parent 

297 

298 return constraints_mapping 

299 

300 

301def _add_ifs_constraints( 

302 expr: _NameNodes, 

303 ifs: list[nodes.NodeNG], 

304 constraints_mapping: dict[nodes.NodeNG, set[Constraint]], 

305) -> None: 

306 """Add the constraints matching each comprehension condition in ifs.""" 

307 for if_expr in ifs: 

308 constraints = set(_match_constraint(expr, if_expr)) 

309 if constraints: 

310 constraints_mapping[if_expr] = constraints 

311 

312 

313ALL_CONSTRAINT_CLASSES = frozenset( 

314 ( 

315 NoneConstraint, 

316 BooleanConstraint, 

317 TypeConstraint, 

318 EqualityConstraint, 

319 ) 

320) 

321"""All supported constraint types.""" 

322 

323 

324def _matches(node1: nodes.NodeNG | bases.Proxy, node2: nodes.NodeNG) -> bool: 

325 """Returns True if the two nodes match.""" 

326 if isinstance(node1, nodes.Name) and isinstance(node2, nodes.Name): 

327 return node1.name == node2.name 

328 if isinstance(node1, nodes.Attribute) and isinstance(node2, nodes.Attribute): 

329 return node1.attrname == node2.attrname and _matches(node1.expr, node2.expr) 

330 if isinstance(node1, nodes.Const) and isinstance(node2, nodes.Const): 

331 return node1.value == node2.value 

332 

333 return False 

334 

335 

336def _match_constraint( 

337 node: _NameNodes, expr: nodes.NodeNG, invert: bool = False 

338) -> Iterator[Constraint]: 

339 """Yields all constraint patterns for node that match.""" 

340 for constraint_cls in ALL_CONSTRAINT_CLASSES: 

341 constraint = constraint_cls.match(node, expr, invert) 

342 if constraint: 

343 yield constraint