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
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 try:
175 types = helpers.class_or_tuple_to_container(self.classinfo, context)
176 matches_checked_types = helpers.object_isinstance(inferred, types, context)
178 if matches_checked_types is util.Uninferable:
179 return True
181 return self.negate ^ matches_checked_types
182 except (InferenceError, AstroidTypeError, MroError):
183 return True
186class EqualityConstraint(Constraint):
187 """Represents a "==" or "!=" constraint."""
189 def __init__(self, node: nodes.NodeNG, operand: nodes.NodeNG, negate: bool) -> None:
190 super().__init__(node=node, negate=negate)
191 self.operand = operand
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:
199 - "node == operand" or "operand == node": use given negate value
200 - "node != operand" or "operand != node": flip negate value
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)
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)
214 return None
216 def satisfied_by(
217 self, inferred: InferenceResult, context: InferenceContext
218 ) -> bool:
219 """Return True for uninferable/ambiguous results, or depending on negate flag:
221 - negate=False: satisfied when both operands are equal.
222 - negate=True: satisfied when both operands are not equal.
224 Only comparisons between constants and callables are supported.
225 """
226 if inferred is util.Uninferable:
227 return True
229 operand_inferred = util.safe_infer(self.operand, context)
230 if operand_inferred is util.Uninferable or operand_inferred is None:
231 return True
233 if isinstance(inferred, nodes.Const) and isinstance(
234 operand_inferred, nodes.Const
235 ):
236 return self.negate ^ (inferred.value == operand_inferred.value)
238 if inferred.callable() and operand_inferred.callable():
239 return self.negate ^ (inferred is operand_inferred)
241 return True
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.
249 The returned dictionary maps the node where the constraint was generated to the
250 corresponding constraint(s).
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))
267 if constraints:
268 constraints_mapping[parent] = constraints
269 current_node = parent
271 return constraints_mapping
274ALL_CONSTRAINT_CLASSES = frozenset(
275 (
276 NoneConstraint,
277 BooleanConstraint,
278 TypeConstraint,
279 EqualityConstraint,
280 )
281)
282"""All supported constraint types."""
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
294 return False
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