diff --git a/changelog/9610.bugfix.rst b/changelog/9610.bugfix.rst new file mode 100644 index 000000000..c8c89c0c9 --- /dev/null +++ b/changelog/9610.bugfix.rst @@ -0,0 +1,3 @@ +Restore `UnitTestFunction.obj` to return unbound rather than bound method. +Fixes a crash during a failed teardown in unittest TestCases with non-default `__init__`. +Regressed in pytest 7.0.0. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index a05c3b4bc..851e4943b 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -185,6 +185,15 @@ class TestCaseFunction(Function): _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None _testcase: Optional["unittest.TestCase"] = None + def _getobj(self): + assert self.parent is not None + # Unlike a regular Function in a Class, where `item.obj` returns + # a *bound* method (attached to an instance), TestCaseFunction's + # `obj` returns an *unbound* method (not attached to an instance). + # This inconsistency is probably not desirable, but needs some + # consideration before changing. + return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] + def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). self._explicit_tearDown: Optional[Callable[[], None]] = None diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 12bcb9361..1601086d5 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1472,3 +1472,29 @@ def test_do_cleanups_on_teardown_failure(pytester: Pytester) -> None: passed, skipped, failed = reprec.countoutcomes() assert failed == 2 assert passed == 1 + + +def test_traceback_pruning(pytester: Pytester) -> None: + """Regression test for #9610 - doesn't crash during traceback pruning.""" + pytester.makepyfile( + """ + import unittest + + class MyTestCase(unittest.TestCase): + def __init__(self, test_method): + unittest.TestCase.__init__(self, test_method) + + class TestIt(MyTestCase): + @classmethod + def tearDownClass(cls) -> None: + assert False + + def test_it(self): + pass + """ + ) + reprec = pytester.inline_run() + passed, skipped, failed = reprec.countoutcomes() + assert passed == 1 + assert failed == 1 + assert reprec.ret == 1