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

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

134 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 try: 

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

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

177 

178 if matches_checked_types is util.Uninferable: 

179 return True 

180 

181 return self.negate ^ matches_checked_types 

182 except (InferenceError, AstroidTypeError, MroError): 

183 return True 

184 

185 

186class EqualityConstraint(Constraint): 

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

188 

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

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

191 self.operand = operand 

192 

193 @classmethod 

194 def match( 

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

196 ) -> Self | None: 

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

198 

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

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

201 

202 Return None if no pattern matches. 

203 """ 

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

205 left = expr.left 

206 op, right = expr.ops[0] 

207 matches_left = _matches(left, node) 

208 

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

210 operand = right if matches_left else left 

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

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

213 

214 return None 

215 

216 def satisfied_by( 

217 self, inferred: InferenceResult, context: InferenceContext 

218 ) -> bool: 

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

220 

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

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

223 

224 Only comparisons between constants and callables are supported. 

225 """ 

226 if inferred is util.Uninferable: 

227 return True 

228 

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

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

231 return True 

232 

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

234 operand_inferred, nodes.Const 

235 ): 

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

237 

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

239 return self.negate ^ (inferred is operand_inferred) 

240 

241 return True 

242 

243 

244def get_constraints( 

245 expr: _NameNodes, frame: nodes.LocalsDictNodeNG 

246) -> dict[nodes.If | nodes.IfExp, set[Constraint]]: 

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

248 

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

250 corresponding constraint(s). 

251 

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

253 Currently this only supports constraints generated from if conditions. 

254 """ 

255 current_node: nodes.NodeNG | None = expr 

256 constraints_mapping: dict[nodes.If | nodes.IfExp, set[Constraint]] = {} 

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

258 parent = current_node.parent 

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

260 branch, _ = parent.locate_child(current_node) 

261 constraints: set[Constraint] | None = None 

262 if branch == "body": 

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

264 elif branch == "orelse": 

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

266 

267 if constraints: 

268 constraints_mapping[parent] = constraints 

269 current_node = parent 

270 

271 return constraints_mapping 

272 

273 

274ALL_CONSTRAINT_CLASSES = frozenset( 

275 ( 

276 NoneConstraint, 

277 BooleanConstraint, 

278 TypeConstraint, 

279 EqualityConstraint, 

280 ) 

281) 

282"""All supported constraint types.""" 

283 

284 

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

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

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

288 return node1.name == node2.name 

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

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

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

292 return node1.value == node2.value 

293 

294 return False 

295 

296 

297def _match_constraint( 

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

299) -> Iterator[Constraint]: 

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

301 for constraint_cls in ALL_CONSTRAINT_CLASSES: 

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

303 if constraint: 

304 yield constraint