Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(rewrite): add visit_IfExp for ternary expression introspection
Implement a dedicated visitor for ast.IfExp that introspects the
condition value while preserving short-circuit semantics. Produces
failure messages like:

    assert 0 == 99
     +  where 0 = (... if True else ...)

The condition is rewritten for introspection (showing its evaluated
value), but branches are kept as-is to preserve Python's short-circuit
behavior — only the selected branch is evaluated.

Previously, IfExp hit generic_visit showing only the final result
without any insight into which branch was taken or why.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
  • Loading branch information
3 people committed May 8, 2026
commit 79d59da23469ccc1418eda06e30b13e06ab282ab
14 changes: 14 additions & 0 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,20 @@ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
new_starred = ast.Starred(res, starred.ctx)
return new_starred, "*" + expl

def visit_IfExp(self, ifexp: ast.IfExp) -> tuple[ast.Name, str]:
# Introspect the condition but keep branches as-is to preserve
# short-circuit semantics (only the selected branch is evaluated).
cond_res, cond_expl = self.visit(ifexp.test)
# Reconstruct the IfExp with the rewritten condition but original
# branches to avoid evaluating both sides.
res = self.assign(
ast.copy_location(ast.IfExp(cond_res, ifexp.body, ifexp.orelse), ifexp)
)
res_expl = self.explanation_param(self.display(res))
pat = "%s\n{%s = (... if %s else ...)\n}"
expl = pat % (res_expl, res_expl, cond_expl)
return res, expl

def visit_Subscript(self, subscript: ast.Subscript) -> tuple[ast.Name, str]:
if not isinstance(subscript.ctx, ast.Load):
return self.generic_visit(subscript)
Expand Down
25 changes: 20 additions & 5 deletions testing/test_assertrewrite_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,17 +576,16 @@ def check():


class TestIntrospectionIfExp:
"""Ternary / if-expression — currently hits generic_visit."""
"""Ternary / if-expression — now has dedicated visitor."""

@pytest.mark.xfail(reason="IfExp not introspected: blind spot")
def test_ifexp_shows_condition_and_branch(self) -> None:
def test_ifexp_shows_condition_value(self) -> None:
assert_introspects(
"""
def check():
flag = True
assert (0 if flag else 1) == 1
""",
must_contain=["flag", "True"],
must_contain=["if True else"],
)

def test_ifexp_semantics_preserved(self) -> None:
Expand All @@ -603,9 +602,25 @@ def check():
flag = True
assert (0 if flag else 1) == 99
""",
must_contain=["assert 0 == 99"],
must_contain=["assert 0 == 99", "if True else"],
)

def test_ifexp_short_circuit_true(self) -> None:
"""Orelse branch must NOT be evaluated when condition is True."""
assert_passes_when_true("""
def check():
flag = True
assert (1 if flag else (1/0)) == 1
""")

def test_ifexp_short_circuit_false(self) -> None:
"""Body branch must NOT be evaluated when condition is False."""
assert_passes_when_true("""
def check():
flag = False
assert (1/0 if flag else 1) == 1
""")


class TestIntrospectionContainerLiteral:
"""Container literals ([...], {...}, {k:v}) — currently hits generic_visit."""
Expand Down