Skip to content

Commit 27f14a7

Browse files
committed
Implement type constraint
1 parent 4a7e23f commit 27f14a7

File tree

3 files changed

+78
-27
lines changed

3 files changed

+78
-27
lines changed

astroid/brain/brain_builtin_inference.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def infer_issubclass(callnode, context: InferenceContext | None = None):
763763
# The right hand argument is the class(es) that the given
764764
# object is to be checked against.
765765
try:
766-
class_container = _class_or_tuple_to_container(
766+
class_container = helpers.class_or_tuple_to_container(
767767
class_or_tuple_node, context=context
768768
)
769769
except InferenceError as exc:
@@ -798,7 +798,7 @@ def infer_isinstance(
798798
# The right hand argument is the class(es) that the given
799799
# obj is to be check is an instance of
800800
try:
801-
class_container = _class_or_tuple_to_container(
801+
class_container = helpers.class_or_tuple_to_container(
802802
class_or_tuple_node, context=context
803803
)
804804
except InferenceError as exc:
@@ -814,30 +814,6 @@ def infer_isinstance(
814814
return nodes.Const(isinstance_bool)
815815

816816

817-
def _class_or_tuple_to_container(
818-
node: InferenceResult, context: InferenceContext | None = None
819-
) -> list[InferenceResult]:
820-
# Move inferences results into container
821-
# to simplify later logic
822-
# raises InferenceError if any of the inferences fall through
823-
try:
824-
node_infer = next(node.infer(context=context))
825-
except StopIteration as e:
826-
raise InferenceError(node=node, context=context) from e
827-
# arg2 MUST be a type or a TUPLE of types
828-
# for isinstance
829-
if isinstance(node_infer, nodes.Tuple):
830-
try:
831-
class_container = [
832-
next(node.infer(context=context)) for node in node_infer.elts
833-
]
834-
except StopIteration as e:
835-
raise InferenceError(node=node, context=context) from e
836-
else:
837-
class_container = [node_infer]
838-
return class_container
839-
840-
841817
def infer_len(node, context: InferenceContext | None = None) -> nodes.Const:
842818
"""Infer length calls.
843819

astroid/constraint.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from collections.abc import Iterator
1111
from typing import TYPE_CHECKING
1212

13-
from astroid import nodes, util
13+
from astroid import helpers, nodes, util
14+
from astroid.exceptions import AstroidTypeError, InferenceError, MroError
1415
from astroid.typing import InferenceResult
1516

1617
if sys.version_info >= (3, 11):
@@ -125,6 +126,55 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
125126
return self.negate ^ inferred_booleaness
126127

127128

129+
class TypeConstraint(Constraint):
130+
"""Represents an "isinstance(x, y)" constraint."""
131+
132+
def __init__(
133+
self, node: nodes.NodeNG, classinfo: nodes.NodeNG, negate: bool
134+
) -> None:
135+
super().__init__(node=node, negate=negate)
136+
self.classinfo = classinfo
137+
138+
@classmethod
139+
def match(
140+
cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
141+
) -> Self | None:
142+
"""Return a new constraint for node if expr matches the
143+
"isinstance(x, y)" pattern. Else, return None.
144+
"""
145+
is_instance_call = (
146+
isinstance(expr, nodes.Call)
147+
and isinstance(expr.func, nodes.Name)
148+
and expr.func.name == "isinstance"
149+
and not expr.keywords
150+
and len(expr.args) == 2
151+
)
152+
if is_instance_call and _matches(expr.args[0], node):
153+
return cls(node=node, classinfo=expr.args[1], negate=negate)
154+
155+
return None
156+
157+
def satisfied_by(self, inferred: InferenceResult) -> bool:
158+
"""Return True for uninferable results, or depending on negate flag:
159+
160+
- negate=False: satisfied when inferred is an instance of the checked types.
161+
- negate=True: satisfied when inferred is not an instance of the checked types.
162+
"""
163+
if isinstance(inferred, util.UninferableBase):
164+
return True
165+
166+
try:
167+
types = helpers.class_or_tuple_to_container(self.classinfo)
168+
matches_checked_types = helpers.object_isinstance(inferred, types)
169+
170+
if isinstance(matches_checked_types, util.UninferableBase):
171+
return True
172+
173+
return self.negate ^ matches_checked_types
174+
except (InferenceError, AstroidTypeError, MroError):
175+
return True
176+
177+
128178
def get_constraints(
129179
expr: _NameNodes, frame: nodes.LocalsDictNodeNG
130180
) -> dict[nodes.If | nodes.IfExp, set[Constraint]]:
@@ -159,6 +209,7 @@ def get_constraints(
159209
(
160210
NoneConstraint,
161211
BooleanConstraint,
212+
TypeConstraint,
162213
)
163214
)
164215
"""All supported constraint types."""

astroid/helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,30 @@ def object_issubclass(
170170
return _object_type_is_subclass(node, class_or_seq, context=context)
171171

172172

173+
def class_or_tuple_to_container(
174+
node: InferenceResult, context: InferenceContext | None = None
175+
) -> list[InferenceResult]:
176+
# Move inferences results into container
177+
# to simplify later logic
178+
# raises InferenceError if any of the inferences fall through
179+
try:
180+
node_infer = next(node.infer(context=context))
181+
except StopIteration as e:
182+
raise InferenceError(node=node, context=context) from e
183+
# arg2 MUST be a type or a TUPLE of types
184+
# for isinstance
185+
if isinstance(node_infer, nodes.Tuple):
186+
try:
187+
class_container = [
188+
next(node.infer(context=context)) for node in node_infer.elts
189+
]
190+
except StopIteration as e:
191+
raise InferenceError(node=node, context=context) from e
192+
else:
193+
class_container = [node_infer]
194+
return class_container
195+
196+
173197
def has_known_bases(klass, context: InferenceContext | None = None) -> bool:
174198
"""Return whether all base classes of a class could be inferred."""
175199
try:

0 commit comments

Comments
 (0)