|
10 | 10 | from collections.abc import Iterator |
11 | 11 | from typing import TYPE_CHECKING |
12 | 12 |
|
13 | | -from astroid import nodes, util |
| 13 | +from astroid import helpers, nodes, util |
| 14 | +from astroid.exceptions import AstroidTypeError, InferenceError, MroError |
14 | 15 | from astroid.typing import InferenceResult |
15 | 16 |
|
16 | 17 | if sys.version_info >= (3, 11): |
@@ -77,7 +78,7 @@ def match( |
77 | 78 | def satisfied_by(self, inferred: InferenceResult) -> bool: |
78 | 79 | """Return True if this constraint is satisfied by the given inferred value.""" |
79 | 80 | # Assume true if uninferable |
80 | | - if isinstance(inferred, util.UninferableBase): |
| 81 | + if inferred is util.Uninferable: |
81 | 82 | return True |
82 | 83 |
|
83 | 84 | # Return the XOR of self.negate and matches(inferred, self.CONST_NONE) |
@@ -117,14 +118,61 @@ def satisfied_by(self, inferred: InferenceResult) -> bool: |
117 | 118 | - negate=True: satisfied if boolean value is False |
118 | 119 | """ |
119 | 120 | inferred_booleaness = inferred.bool_value() |
120 | | - if isinstance(inferred, util.UninferableBase) or isinstance( |
121 | | - inferred_booleaness, util.UninferableBase |
122 | | - ): |
| 121 | + if inferred is util.Uninferable or inferred_booleaness is util.Uninferable: |
123 | 122 | return True |
124 | 123 |
|
125 | 124 | return self.negate ^ inferred_booleaness |
126 | 125 |
|
127 | 126 |
|
| 127 | +class TypeConstraint(Constraint): |
| 128 | + """Represents an "isinstance(x, y)" constraint.""" |
| 129 | + |
| 130 | + def __init__( |
| 131 | + self, node: nodes.NodeNG, classinfo: nodes.NodeNG, negate: bool |
| 132 | + ) -> None: |
| 133 | + super().__init__(node=node, negate=negate) |
| 134 | + self.classinfo = classinfo |
| 135 | + |
| 136 | + @classmethod |
| 137 | + def match( |
| 138 | + cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False |
| 139 | + ) -> Self | None: |
| 140 | + """Return a new constraint for node if expr matches the |
| 141 | + "isinstance(x, y)" pattern. Else, return None. |
| 142 | + """ |
| 143 | + is_instance_call = ( |
| 144 | + isinstance(expr, nodes.Call) |
| 145 | + and isinstance(expr.func, nodes.Name) |
| 146 | + and expr.func.name == "isinstance" |
| 147 | + and not expr.keywords |
| 148 | + and len(expr.args) == 2 |
| 149 | + ) |
| 150 | + if is_instance_call and _matches(expr.args[0], node): |
| 151 | + return cls(node=node, classinfo=expr.args[1], negate=negate) |
| 152 | + |
| 153 | + return None |
| 154 | + |
| 155 | + def satisfied_by(self, inferred: InferenceResult) -> bool: |
| 156 | + """Return True for uninferable results, or depending on negate flag: |
| 157 | +
|
| 158 | + - negate=False: satisfied when inferred is an instance of the checked types. |
| 159 | + - negate=True: satisfied when inferred is not an instance of the checked types. |
| 160 | + """ |
| 161 | + if inferred is util.Uninferable: |
| 162 | + return True |
| 163 | + |
| 164 | + try: |
| 165 | + types = helpers.class_or_tuple_to_container(self.classinfo) |
| 166 | + matches_checked_types = helpers.object_isinstance(inferred, types) |
| 167 | + |
| 168 | + if matches_checked_types is util.Uninferable: |
| 169 | + return True |
| 170 | + |
| 171 | + return self.negate ^ matches_checked_types |
| 172 | + except (InferenceError, AstroidTypeError, MroError): |
| 173 | + return True |
| 174 | + |
| 175 | + |
128 | 176 | def get_constraints( |
129 | 177 | expr: _NameNodes, frame: nodes.LocalsDictNodeNG |
130 | 178 | ) -> dict[nodes.If | nodes.IfExp, set[Constraint]]: |
@@ -159,6 +207,7 @@ def get_constraints( |
159 | 207 | ( |
160 | 208 | NoneConstraint, |
161 | 209 | BooleanConstraint, |
| 210 | + TypeConstraint, |
162 | 211 | ) |
163 | 212 | ) |
164 | 213 | """All supported constraint types.""" |
|
0 commit comments