Skip to content
Closed
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
1 change: 1 addition & 0 deletions changelog/9582.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed assertion rewriting crash in class bodies with custom metaclasses (e.g. ``Enum``) that reject duplicate namespace keys -- by :user:`mturac`.
12 changes: 11 additions & 1 deletion src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,11 +714,21 @@ def run(self, mod: ast.Module) -> None:
while nodes:
node = nodes.pop()
assert isinstance(node, ast.AST)
# Skip assertion rewriting inside class bodies: the rewriter
# injects temporary variables (@py_assert0, etc.) which are
# then cleaned up by assigning None to the same name. Class
# namespaces that reject duplicate keys (e.g. Enum) raise
# TypeError on the second assignment. Methods inside the
# class are still rewritten because they are FunctionDef
# nodes whose own bodies are visited separately.
in_class_body = isinstance(node, ast.ClassDef)
for name, field in ast.iter_fields(node):
if isinstance(field, list):
new: list[ast.AST] = []
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
if isinstance(child, ast.Assert) and not (
in_class_body and name == "body"
):
# Transform assert.
new.extend(self.visit(child))
else:
Expand Down
41 changes: 41 additions & 0 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -2390,3 +2390,44 @@ def test():
)
reprec = pytester.inline_run("-p", "no:terminalreporter")
reprec.assertoutcome(passed=1)


def test_enum_class_body_assert_not_rewritten(pytester: Pytester) -> None:
"""Assertion rewriting must not inject temporary variables into class
bodies, because class namespaces that reject duplicate keys (e.g. Enum)
raise TypeError on the cleanup assignment. (#9582)"""
pytester.makepyfile(
"""
from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
assert RED is not None

def test_enum_values():
assert Color.RED.value == 1
assert Color.BLUE.value == 3
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=1)


def test_class_body_assert_methods_still_rewritten(pytester: Pytester) -> None:
"""Methods inside a class should still get assertion rewriting even
though class-body asserts are skipped. (#9582)"""
pytester.makepyfile(
"""
class TestSomething:
assert True # class body -- not rewritten

def test_detailed_failure(self):
x = 1
assert x == 2
"""
)
result = pytester.runpytest()
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(["*assert 1 == 2*"])