Skip to content

Commit ae65813

Browse files
feat(rewrite): introspect container[key] in failure messages
A subscript was opaque: the message showed the value it produced with no indication of which container or key it came from. Decompose it the way attribute access already is. The container goes through visit_operand() because taking the expression away from generic_visit() takes away the hoisting that kept it ordered -- without that, `assert box[identity(box := other)] == 1` would start reading the post-walrus container. The order-axis guard in the coverage matrix fails if this is dropped. Slices keep the generic treatment; decomposing start/stop/step is rarely what a failure message needs. Closes the introspect-subscript group in the coverage matrix.
1 parent 6cd2885 commit ae65813

3 files changed

Lines changed: 20 additions & 13 deletions

File tree

‎changelog/14815.improvement.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Assertion failure messages now decompose subscript expressions, showing the container and the key that produced a value::
2+
3+
assert 1 == 99
4+
+ where 1 = {'a': 1, 'b': 2}['a']

‎src/_pytest/assertion/rewrite.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,21 @@ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
11191119
new_starred = ast.Starred(res, starred.ctx)
11201120
return new_starred, "*" + expl
11211121

1122+
def visit_Subscript(self, subscript: ast.Subscript) -> tuple[ast.Name, str]:
1123+
# For Slice objects (a[1:3]), fall back to generic — decomposing
1124+
# start/stop/step is rarely useful in assertion messages.
1125+
if isinstance(subscript.slice, ast.Slice):
1126+
return self.generic_visit(subscript)
1127+
value, value_expl = self.visit_operand(subscript.value, [subscript.slice])
1128+
slice_res, slice_expl = self.visit(subscript.slice)
1129+
res = self.assign(
1130+
ast.copy_location(ast.Subscript(value, slice_res, ast.Load()), subscript)
1131+
)
1132+
res_expl = self.explanation_param(self.display(res))
1133+
pat = "%s\n{%s = %s[%s]\n}"
1134+
expl = pat % (res_expl, res_expl, value_expl, slice_expl)
1135+
return res, expl
1136+
11221137
def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
11231138
value, value_expl = self.visit(attr.value)
11241139
res = self.assign(

‎testing/test_assertrewrite_coverage.py‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -601,12 +601,8 @@ def check():
601601

602602

603603
class TestIntrospectionSubscript:
604-
"""Subscript / indexing — currently hits generic_visit."""
604+
"""Subscript / indexing — has a dedicated visitor."""
605605

606-
@pytest.mark.xfail(
607-
strict=True,
608-
reason="introspect-subscript: container[key] is not decomposed",
609-
)
610606
def test_dict_subscript_shows_key_and_container(self) -> None:
611607
assert_introspects(
612608
"""
@@ -617,10 +613,6 @@ def check():
617613
must_contain=["where 1 = ", "['a']"],
618614
)
619615

620-
@pytest.mark.xfail(
621-
strict=True,
622-
reason="introspect-subscript: container[key] is not decomposed",
623-
)
624616
def test_list_subscript_shows_index_and_container(self) -> None:
625617
assert_introspects(
626618
"""
@@ -1389,10 +1381,6 @@ def identity(v):
13891381
class TestEdgeCases:
13901382
"""Regression and edge-case tests combining multiple expression types."""
13911383

1392-
@pytest.mark.xfail(
1393-
strict=True,
1394-
reason="introspect-subscript: container[key] is not decomposed",
1395-
)
13961384
def test_subscript_with_variable_key(self) -> None:
13971385
"""Subscript where the key is a variable (not constant)."""
13981386
assert_introspects(

0 commit comments

Comments
 (0)