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: 5 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ What's New in astroid 4.0.3?
============================
Release date: TBA

* Fix inference of ``IfExp`` (ternary expression) nodes to avoid prematurely narrowing
results in the face of inference ambiguity.

Closes #2899

* Fix base class inference for dataclasses using the PEP 695 typing syntax.

Refs pylint-dev/pylint#10788



What's New in astroid 4.0.2?
============================
Release date: 2025-11-09
Expand Down
40 changes: 23 additions & 17 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3108,31 +3108,37 @@ def _infer(
to inferring both branches. Otherwise, we infer either branch
depending on the condition.
"""
both_branches = False

# We use two separate contexts for evaluating lhs and rhs because
# evaluating lhs may leave some undesired entries in context.path
# which may not let us infer right value of rhs.

context = context or InferenceContext()
lhs_context = copy_context(context)
rhs_context = copy_context(context)

# Infer bool condition. Stop inferring if in doubt and fallback to
# evaluating both branches.
condition: bool | None = None
try:
test = next(self.test.infer(context=context.clone()))
except (InferenceError, StopIteration):
both_branches = True
else:
test_bool_value = test.bool_value()
if not isinstance(test, util.UninferableBase) and not isinstance(
test_bool_value, util.UninferableBase
):
if test_bool_value:
yield from self.body.infer(context=lhs_context)
else:
yield from self.orelse.infer(context=rhs_context)
else:
both_branches = True
if both_branches:
for test in self.test.infer(context=context.clone()):
if isinstance(test, util.UninferableBase):
condition = None
break
test_bool_value = test.bool_value()
if isinstance(test_bool_value, util.UninferableBase):
condition = None
break
if condition is None:
condition = test_bool_value
elif test_bool_value != condition:
condition = None
break
except InferenceError:
condition = None

if condition is True or condition is None:
yield from self.body.infer(context=lhs_context)
if condition is False or condition is None:
yield from self.orelse.infer(context=rhs_context)


Expand Down
92 changes: 92 additions & 0 deletions tests/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -6442,6 +6442,98 @@ def both_branches():
assert [third[0].value, third[1].value] == [1, 2]


def test_ifexp_with_default_arguments() -> None:
code = """
def with_default(foo: str | None = None):
a = 1 if foo else "bar" #@

def without_default(foo: str):
a = 1 if foo else "bar" #@

def some_ifexps(foo: str | None = None):
a = 1 if foo else 2
b = 3 if a else 4 #@
c = 4 if b else 5 #@
d = 5 if not foo else foo #@
e = d if not foo else foo #@
"""

ast_nodes = extract_node(code)

first = ast_nodes[0].value.inferred()
second = ast_nodes[1].value.inferred()
third = ast_nodes[2].value.inferred()
fourth = ast_nodes[3].value.inferred()
fifth = ast_nodes[4].value.inferred()
sixth = ast_nodes[5].value.inferred()

assert len(first) == 2
assert [first[0].value, first[1].value] == [1, "bar"]

assert len(second) == 2
assert [second[0].value, second[1].value] == [1, "bar"]

assert len(third) == 1
assert third[0].value == 3

assert len(fourth) == 1
assert fourth[0].value == 4

assert len(fifth) == 2
assert [fifth[0].value, fifth[1].value] == [5, Uninferable]

assert len(sixth) == 3
assert [sixth[0].value, sixth[1].value, sixth[2].value] == [
5,
Uninferable,
Uninferable,
]


def test_ifexp_with_uninferables() -> None:
code = """
def truthy_and_falsy():
return False if unknown() else True

def truthy_and_uninferable():
return False if unknown() else unknown()

def calls_truthy_and_falsy():
return 1 if truthy_and_falsy() else 2

def calls_truthy_and_uninferable():
return 1 if range(10) else truthy_and_uninferable()

truthy_and_falsy() #@
truthy_and_uninferable() #@
calls_truthy_and_falsy() #@
calls_truthy_and_uninferable() #@
"""

ast_nodes = extract_node(code)

first = ast_nodes[0].inferred()
second = ast_nodes[1].inferred()
third = ast_nodes[2].inferred()
fourth = ast_nodes[3].inferred()

assert len(first) == 2
assert [first[0].value, first[1].value] == [False, True]

assert len(second) == 2
assert [second[0].value, second[1].value] == [False, Uninferable]

assert len(third) == 2
assert [third[0].value, third[1].value] == [1, 2]

assert len(fourth) == 3
assert [fourth[0].value, fourth[1].value, fourth[2].value] == [
1,
False,
Uninferable,
]


def test_assert_last_function_returns_none_on_inference() -> None:
code = """
def check_equal(a, b):
Expand Down