Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ What's New in astroid 4.1.0?
============================
Release date: TBA

* Add support for type constraints (`isinstance(x, y)`) in inference.

Closes pylint-dev/pylint#1162
Closes pylint-dev/pylint#4635
Closes pylint-dev/pylint#10469

* Make `type.__new__()` raise clear errors instead of returning `None`

* Move object dunder methods from ``FunctionModel`` to ``ObjectModel`` to make them
Expand Down
28 changes: 2 additions & 26 deletions astroid/brain/brain_builtin_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ def infer_issubclass(callnode, context: InferenceContext | None = None):
# The right hand argument is the class(es) that the given
# object is to be checked against.
try:
class_container = _class_or_tuple_to_container(
class_container = helpers.class_or_tuple_to_container(
class_or_tuple_node, context=context
)
except InferenceError as exc:
Expand Down Expand Up @@ -798,7 +798,7 @@ def infer_isinstance(
# The right hand argument is the class(es) that the given
# obj is to be check is an instance of
try:
class_container = _class_or_tuple_to_container(
class_container = helpers.class_or_tuple_to_container(
class_or_tuple_node, context=context
)
except InferenceError as exc:
Expand All @@ -814,30 +814,6 @@ def infer_isinstance(
return nodes.Const(isinstance_bool)


def _class_or_tuple_to_container(
node: InferenceResult, context: InferenceContext | None = None
) -> list[InferenceResult]:
# Move inferences results into container
# to simplify later logic
# raises InferenceError if any of the inferences fall through
try:
node_infer = next(node.infer(context=context))
except StopIteration as e:
raise InferenceError(node=node, context=context) from e
# arg2 MUST be a type or a TUPLE of types
# for isinstance
if isinstance(node_infer, nodes.Tuple):
try:
class_container = [
next(node.infer(context=context)) for node in node_infer.elts
]
except StopIteration as e:
raise InferenceError(node=node, context=context) from e
else:
class_container = [node_infer]
return class_container


def infer_len(node, context: InferenceContext | None = None) -> nodes.Const:
"""Infer length calls.

Expand Down
59 changes: 54 additions & 5 deletions astroid/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from collections.abc import Iterator
from typing import TYPE_CHECKING

from astroid import nodes, util
from astroid import helpers, nodes, util
from astroid.exceptions import AstroidTypeError, InferenceError, MroError
from astroid.typing import InferenceResult

if sys.version_info >= (3, 11):
Expand Down Expand Up @@ -77,7 +78,7 @@ def match(
def satisfied_by(self, inferred: InferenceResult) -> bool:
"""Return True if this constraint is satisfied by the given inferred value."""
# Assume true if uninferable
if isinstance(inferred, util.UninferableBase):
if inferred is util.Uninferable:
return True

# Return the XOR of self.negate and matches(inferred, self.CONST_NONE)
Expand Down Expand Up @@ -117,14 +118,61 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
- negate=True: satisfied if boolean value is False
"""
inferred_booleaness = inferred.bool_value()
if isinstance(inferred, util.UninferableBase) or isinstance(
inferred_booleaness, util.UninferableBase
):
if inferred is util.Uninferable or inferred_booleaness is util.Uninferable:
return True

return self.negate ^ inferred_booleaness


class TypeConstraint(Constraint):
"""Represents an "isinstance(x, y)" constraint."""

def __init__(
self, node: nodes.NodeNG, classinfo: nodes.NodeNG, negate: bool
) -> None:
super().__init__(node=node, negate=negate)
self.classinfo = classinfo

@classmethod
def match(
cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
) -> Self | None:
"""Return a new constraint for node if expr matches the
"isinstance(x, y)" pattern. Else, return None.
"""
is_instance_call = (
isinstance(expr, nodes.Call)
and isinstance(expr.func, nodes.Name)
and expr.func.name == "isinstance"
and not expr.keywords
and len(expr.args) == 2
)
if is_instance_call and _matches(expr.args[0], node):
return cls(node=node, classinfo=expr.args[1], negate=negate)

return None

def satisfied_by(self, inferred: InferenceResult) -> bool:
"""Return True for uninferable results, or depending on negate flag:

- negate=False: satisfied when inferred is an instance of the checked types.
- negate=True: satisfied when inferred is not an instance of the checked types.
"""
if inferred is util.Uninferable:
return True

try:
types = helpers.class_or_tuple_to_container(self.classinfo)
matches_checked_types = helpers.object_isinstance(inferred, types)

if matches_checked_types is util.Uninferable:
return True

return self.negate ^ matches_checked_types
except (InferenceError, AstroidTypeError, MroError):
return True


def get_constraints(
expr: _NameNodes, frame: nodes.LocalsDictNodeNG
) -> dict[nodes.If | nodes.IfExp, set[Constraint]]:
Expand Down Expand Up @@ -159,6 +207,7 @@ def get_constraints(
(
NoneConstraint,
BooleanConstraint,
TypeConstraint,
)
)
"""All supported constraint types."""
Expand Down
24 changes: 24 additions & 0 deletions astroid/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ def object_issubclass(
return _object_type_is_subclass(node, class_or_seq, context=context)


def class_or_tuple_to_container(
node: InferenceResult, context: InferenceContext | None = None
) -> list[InferenceResult]:
# Move inferences results into container
# to simplify later logic
# raises InferenceError if any of the inferences fall through
try:
node_infer = next(node.infer(context=context))
except StopIteration as e: # pragma: no cover
raise InferenceError(node=node, context=context) from e
# arg2 MUST be a type or a TUPLE of types
# for isinstance
if isinstance(node_infer, nodes.Tuple):
try:
class_container = [
next(node.infer(context=context)) for node in node_infer.elts
]
except StopIteration as e: # pragma: no cover
raise InferenceError(node=node, context=context) from e
else:
class_container = [node_infer]
return class_container


def has_known_bases(klass, context: InferenceContext | None = None) -> bool:
"""Return whether all base classes of a class could be inferred."""
try:
Expand Down
Loading
Loading