From d00ca3f8e57c38b77b554d9826181f45de714ba6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 7 Feb 2022 00:25:32 +0200 Subject: [PATCH] unittest: restore `UnitTestFunction.obj` to return unbound rather than bound method This fixes #9610. pytest 7.0.0 (unintentionally) changed `UnitTestFunction.obj`'s' behavior to match `Function.obj`. That is probably a good thing to have, however it evidently causes some regressions as described in the issue, so restore the previous behavior for now. In the future we might want to make this change again, but with proper consideration. --- changelog/9610.bugfix.rst | 3 +++ src/_pytest/unittest.py | 9 +++++++++ testing/test_unittest.py | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 changelog/9610.bugfix.rst diff --git a/changelog/9610.bugfix.rst b/changelog/9610.bugfix.rst new file mode 100644 index 0000000000..c8c89c0c96 --- /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 a05c3b4bc4..851e4943b2 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 12bcb9361a..1601086d5b 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1472,3 +1472,29 @@ def test_cleanup_called_the_right_number_of_times(): 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