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
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
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
5"""Classes representing different types of constraints on inference values."""
7from __future__ import annotations
9import sys
10from abc import ABC, abstractmethod
11from collections.abc import Iterator
12from typing import TYPE_CHECKING
14from astroid import helpers, nodes, util
15from astroid.context import InferenceContext
16from astroid.exceptions import AstroidTypeError, InferenceError, MroError
17from astroid.typing import InferenceResult
19if sys.version_info >= (3, 11):
20 from typing import Self
21else:
22 from typing_extensions import Self
24if TYPE_CHECKING:
25 from astroid import bases
27_NameNodes = nodes.AssignAttr | nodes.Attribute | nodes.AssignName | nodes.Name
30class Constraint(ABC):
31 """Represents a single constraint on a variable."""
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"."""
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.
47 If negate is True, negate the constraint.
48 """
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."""
57class NoneConstraint(Constraint):
58 """Represents an "is None" or "is not None" constraint."""
60 CONST_NONE: nodes.Const = nodes.Const(None)
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.
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)
80 return None
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
90 # Return the XOR of self.negate and matches(inferred, self.CONST_NONE)
91 return self.negate ^ _matches(inferred, self.CONST_NONE)
94class BooleanConstraint(Constraint):
95 """Represents an "x" or "not x" constraint."""
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:
103 - direct match (expr == node): use given negate value
104 - negated match (expr == `not node`): flip negate value
106 Return None if no pattern matches.
107 """
108 if _matches(expr, node):
109 return cls(node=node, negate=negate)
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)
118 return None
120 def satisfied_by(
121 self, inferred: InferenceResult, context: InferenceContext
122 ) -> bool:
123 """Return True for uninferable results, or depending on negate flag:
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
132 return self.negate ^ inferred_booleaness
135class TypeConstraint(Constraint):
136 """Represents an "isinstance(x, y)" constraint."""
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
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)
161 return None
163 def satisfied_by(
164 self, inferred: InferenceResult, context: InferenceContext
165 ) -> bool:
166 """Return True for uninferable results, or depending on negate flag:
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
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)
184 if matches_checked_types is util.Uninferable:
185 return True
187 return self.negate ^ matches_checked_types
188 except (InferenceError, AstroidTypeError, MroError):
189 return True
192class EqualityConstraint(Constraint):
193 """Represents a "==" or "!=" constraint."""
195 def __init__(self, node: nodes.NodeNG, operand: nodes.NodeNG, negate: bool) -> None:
196 super().__init__(node=node, negate=negate)
197 self.operand = operand
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:
205 - "node == operand" or "operand == node": use given negate value
206 - "node != operand" or "operand != node": flip negate value
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)
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)
220 return None
222 def satisfied_by(
223 self, inferred: InferenceResult, context: InferenceContext
224 ) -> bool:
225 """Return True for uninferable/ambiguous results, or depending on negate flag:
227 - negate=False: satisfied when both operands are equal.
228 - negate=True: satisfied when both operands are not equal.
230 Only comparisons between constants and callables are supported.
231 """
232 if inferred is util.Uninferable:
233 return True
235 operand_inferred = util.safe_infer(self.operand, context)
236 if operand_inferred is util.Uninferable or operand_inferred is None:
237 return True
239 if isinstance(inferred, nodes.Const) and isinstance(
240 operand_inferred, nodes.Const
241 ):
242 return self.negate ^ (inferred.value == operand_inferred.value)
244 if inferred.callable() and operand_inferred.callable():
245 return self.negate ^ (inferred is operand_inferred)
247 return True
250def get_constraints(
251 expr: _NameNodes, frame: nodes.LocalsDictNodeNG
252) -> dict[nodes.NodeNG, set[Constraint]]:
253 """Returns the constraints for the given expression.
255 The returned dictionary maps the node where the constraint was generated to the
256 corresponding constraint(s).
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))
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
298 return constraints_mapping
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
313ALL_CONSTRAINT_CLASSES = frozenset(
314 (
315 NoneConstraint,
316 BooleanConstraint,
317 TypeConstraint,
318 EqualityConstraint,
319 )
320)
321"""All supported constraint types."""
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
333 return False
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