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 a ``TypeError`` during collection when a class body contained an ``assert`` statement and the class used a namespace with restricted semantics, such as :class:`enum.Enum` subclasses: asserts in class bodies are no longer rewritten, while asserts inside methods remain rewritten as before.
14 changes: 12 additions & 2 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,8 +744,18 @@ def run(self, mod: ast.Module) -> None:
new: list[ast.AST] = []
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
# Transform assert.
new.extend(self.visit(child))
if isinstance(self.scope[-1], ast.ClassDef):
# Don't rewrite asserts directly in class
# bodies: the class execution namespace may
# have special semantics (e.g. Enum forbids
# reusing keys), which our temporary variables
# would break (#9582). Asserts inside methods
# are still rewritten, as functions get their
# own scope.
new.append(child)
else:
# Transform assert.
new.extend(self.visit(child))
else:
new.append(child)
if isinstance(child, ast.AST):
Expand Down
84 changes: 84 additions & 0 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,90 @@ def test_dont_rewrite_plugin(self, pytester: Pytester) -> None:
result = pytester.runpytest_subprocess()
assert "warning" not in "".join(result.outlines)

def test_dont_rewrite_class_body_assert(self) -> None:
"""Asserts in class bodies are not rewritten (#9582)."""
# Note: body[0] and body[1] are the special imports inserted by the
# rewriter; the class definition starts at body[2].
m = rewrite("class A:\n assert True")
assert isinstance(m.body[2], ast.ClassDef)
assert isinstance(m.body[2].body[0], ast.Assert)

# Asserts in control flow of a class body are not rewritten either.
m = rewrite("class A:\n if True:\n assert True")
class_def = m.body[2]
assert isinstance(class_def, ast.ClassDef)
if_stmt = class_def.body[0]
assert isinstance(if_stmt, ast.If)
assert isinstance(if_stmt.body[0], ast.Assert)

# But asserts in methods are still rewritten.
m = rewrite("class A:\n def f(self):\n assert True")
class_def = m.body[2]
assert isinstance(class_def, ast.ClassDef)
func_def = class_def.body[0]
assert isinstance(func_def, ast.FunctionDef)
assert not any(isinstance(node, ast.Assert) for node in ast.walk(func_def))

def test_assert_in_enum_class_body(self, pytester: Pytester) -> None:
"""Asserts in Enum class bodies no longer break collection (#9582)."""
pytester.makepyfile(
"""
from enum import Enum

STEP = 100

class SomeEnum(Enum):
FIRST = 100
SECOND = FIRST + STEP
THIRD = SECOND + STEP

assert THIRD == 300

def test_enum():
assert SomeEnum.THIRD.value == 300
"""
)
pytester.runpytest().assert_outcomes(passed=1)

def test_asserts_in_enum_class_body(self, pytester: Pytester) -> None:
"""Multiple asserts in one Enum class body (#9582)."""
pytester.makepyfile(
"""
from enum import Enum

class SomeEnum(Enum):
FIRST = 100
SECOND = 200

assert FIRST < SECOND
assert SECOND == 200

def test_enum():
assert SomeEnum.SECOND.value == 200
"""
)
pytester.runpytest().assert_outcomes(passed=1)

def test_assert_in_class_method_still_rewritten(self, pytester: Pytester) -> None:
"""Asserts inside methods of classes are still rewritten (#9582)."""
pytester.makepyfile(
"""
from enum import Enum

class SomeEnum(Enum):
FIRST = 1

def check(self):
assert self.value == 2

def test_method():
SomeEnum.FIRST.check()
"""
)
result = pytester.runpytest()
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(["*assert 1 == 2*"])

def test_rewrites_plugin_as_a_package(self, pytester: Pytester) -> None:
pkgdir = pytester.mkpydir("plugin")
pkgdir.joinpath("__init__.py").write_text(
Expand Down