Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/constraint.py: 51%
67 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:53 +0000
« 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
5"""Classes representing different types of constraints on inference values."""
6from __future__ import annotations
8import sys
9from abc import ABC, abstractmethod
10from collections.abc import Iterator
11from typing import Union
13from astroid import bases, nodes, util
14from astroid.typing import InferenceResult
16if sys.version_info >= (3, 11):
17 from typing import Self
18else:
19 from typing_extensions import Self
21_NameNodes = Union[nodes.AssignAttr, nodes.Attribute, nodes.AssignName, nodes.Name]
24class Constraint(ABC):
25 """Represents a single constraint on a variable."""
27 def __init__(self, node: nodes.NodeNG, negate: bool) -> None:
28 self.node = node
29 """The node that this constraint applies to."""
30 self.negate = negate
31 """True if this constraint is negated. E.g., "is not" instead of "is"."""
33 @classmethod
34 @abstractmethod
35 def match(
36 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
37 ) -> Self | None:
38 """Return a new constraint for node matched from expr, if expr matches
39 the constraint pattern.
41 If negate is True, negate the constraint.
42 """
44 @abstractmethod
45 def satisfied_by(self, inferred: InferenceResult) -> bool:
46 """Return True if this constraint is satisfied by the given inferred value."""
49class NoneConstraint(Constraint):
50 """Represents an "is None" or "is not None" constraint."""
52 CONST_NONE: nodes.Const = nodes.Const(None)
54 @classmethod
55 def match(
56 cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
57 ) -> Self | None:
58 """Return a new constraint for node matched from expr, if expr matches
59 the constraint pattern.
61 Negate the constraint based on the value of negate.
62 """
63 if isinstance(expr, nodes.Compare) and len(expr.ops) == 1:
64 left = expr.left
65 op, right = expr.ops[0]
66 if op in {"is", "is not"} and (
67 _matches(left, node) and _matches(right, cls.CONST_NONE)
68 ):
69 negate = (op == "is" and negate) or (op == "is not" and not negate)
70 return cls(node=node, negate=negate)
72 return None
74 def satisfied_by(self, inferred: InferenceResult) -> bool:
75 """Return True if this constraint is satisfied by the given inferred value."""
76 # Assume true if uninferable
77 if isinstance(inferred, util.UninferableBase):
78 return True
80 # Return the XOR of self.negate and matches(inferred, self.CONST_NONE)
81 return self.negate ^ _matches(inferred, self.CONST_NONE)
84def get_constraints(
85 expr: _NameNodes, frame: nodes.LocalsDictNodeNG
86) -> dict[nodes.If, set[Constraint]]:
87 """Returns the constraints for the given expression.
89 The returned dictionary maps the node where the constraint was generated to the
90 corresponding constraint(s).
92 Constraints are computed statically by analysing the code surrounding expr.
93 Currently this only supports constraints generated from if conditions.
94 """
95 current_node: nodes.NodeNG | None = expr
96 constraints_mapping: dict[nodes.If, set[Constraint]] = {}
97 while current_node is not None and current_node is not frame:
98 parent = current_node.parent
99 if isinstance(parent, nodes.If):
100 branch, _ = parent.locate_child(current_node)
101 constraints: set[Constraint] | None = None
102 if branch == "body":
103 constraints = set(_match_constraint(expr, parent.test))
104 elif branch == "orelse":
105 constraints = set(_match_constraint(expr, parent.test, invert=True))
107 if constraints:
108 constraints_mapping[parent] = constraints
109 current_node = parent
111 return constraints_mapping
114ALL_CONSTRAINT_CLASSES = frozenset((NoneConstraint,))
115"""All supported constraint types."""
118def _matches(node1: nodes.NodeNG | bases.Proxy, node2: nodes.NodeNG) -> bool:
119 """Returns True if the two nodes match."""
120 if isinstance(node1, nodes.Name) and isinstance(node2, nodes.Name):
121 return node1.name == node2.name
122 if isinstance(node1, nodes.Attribute) and isinstance(node2, nodes.Attribute):
123 return node1.attrname == node2.attrname and _matches(node1.expr, node2.expr)
124 if isinstance(node1, nodes.Const) and isinstance(node2, nodes.Const):
125 return node1.value == node2.value
127 return False
130def _match_constraint(
131 node: _NameNodes, expr: nodes.NodeNG, invert: bool = False
132) -> Iterator[Constraint]:
133 """Yields all constraint patterns for node that match."""
134 for constraint_cls in ALL_CONSTRAINT_CLASSES:
135 constraint = constraint_cls.match(node, expr, invert)
136 if constraint:
137 yield constraint