gh-134079: Add addCleanup, enterContext and doCleanups to unittest.subTest and tests#134318
gh-134079: Add addCleanup, enterContext and doCleanups to unittest.subTest and tests#134318ArunRawat404 wants to merge 4 commits intopython:mainfrom
addCleanup, enterContext and doCleanups to unittest.subTest and tests#134318Conversation
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
Lib/unittest/case.py
Outdated
| return "{} {}".format(self.test_case, self._subDescription()) | ||
|
|
||
|
|
||
| class _SubTestCleanupHelper(): |
There was a problem hiding this comment.
| class _SubTestCleanupHelper(): | |
| class _SubTestCleanupHelper: |
Lib/unittest/case.py
Outdated
| def _callCleanup(self, function, /, *args, **kwargs): | ||
| function(*args, **kwargs) |
There was a problem hiding this comment.
| def _callCleanup(self, function, /, *args, **kwargs): | |
| function(*args, **kwargs) | |
| @staticmethod | |
| def _callCleanup(function, /, *args, **kwargs): | |
| function(*args, **kwargs) |
I don't think we need to hold a reference to self so a staticmethod can be used but OTOH, I don't know if there is a world where we want to be able to subclass this one in the future.
| if hasattr(TestSubTestCleanups, '_active_instance'): | ||
| del TestSubTestCleanups._active_instance | ||
|
|
There was a problem hiding this comment.
| if hasattr(TestSubTestCleanups, '_active_instance'): | |
| del TestSubTestCleanups._active_instance | |
| if hasattr(TestSubTestCleanups, '_active_instance'): | |
| del TestSubTestCleanups._active_instance | |
There was a problem hiding this comment.
Can you explain this one i don't see any difference
There was a problem hiding this comment.
We use 2 blank lines to separate toplevel classes/functions and 1 line for methods.
| ] | ||
| self.assertEqual(self.events, expected_events) | ||
|
|
||
| def tearDown(self): |
There was a problem hiding this comment.
Can you put the tearDown() after the setUp() instead? it'll be easier to maintain and read.
| def test_addCleanup_operation_and_LIFO_order(self): | ||
| class MyTests(unittest.TestCase): | ||
| def runTest(inner_self): | ||
| events = TestSubTestCleanups._get_events() | ||
| recorder = TestSubTestCleanups._active_instance._record_event | ||
| with inner_self.subTest() as sub: | ||
| events.append("subtest_body_start") | ||
| sub.addCleanup(recorder, "cleanup_2_args", "arg") | ||
| sub.addCleanup(recorder, "cleanup_1") | ||
| events.append("subtest_body_end") | ||
|
|
||
| MyTests().run() | ||
|
|
||
| expected_events = [ | ||
| "subtest_body_start", | ||
| "subtest_body_end", | ||
| "cleanup_1", | ||
| "cleanup_2_args", | ||
| ] | ||
| self.assertEqual(self.events, expected_events) |
There was a problem hiding this comment.
There should be no need for _active_events_list and _get_events and _record_event.
| def test_addCleanup_operation_and_LIFO_order(self): | |
| class MyTests(unittest.TestCase): | |
| def runTest(inner_self): | |
| events = TestSubTestCleanups._get_events() | |
| recorder = TestSubTestCleanups._active_instance._record_event | |
| with inner_self.subTest() as sub: | |
| events.append("subtest_body_start") | |
| sub.addCleanup(recorder, "cleanup_2_args", "arg") | |
| sub.addCleanup(recorder, "cleanup_1") | |
| events.append("subtest_body_end") | |
| MyTests().run() | |
| expected_events = [ | |
| "subtest_body_start", | |
| "subtest_body_end", | |
| "cleanup_1", | |
| "cleanup_2_args", | |
| ] | |
| self.assertEqual(self.events, expected_events) | |
| def test_addCleanup_operation_and_LIFO_order(self): | |
| events = [] | |
| class MyTests(unittest.TestCase): | |
| def runTest(inner_self): | |
| def record(event_name, *args, **kwargs): | |
| events.append((event_name, args, kwargs)) | |
| with inner_self.subTest() as sub: | |
| events.append("subtest_body_start") | |
| sub.addCleanup(record, "cleanup_2_args", "pos", kw="kwd") | |
| sub.addCleanup(record, "cleanup_1") | |
| events.append("subtest_body_end") | |
| MyTests().run() | |
| expected_events = [ | |
| "subtest_body_start", | |
| "subtest_body_end", | |
| ("cleanup_1", (), {}), | |
| ("cleanup_2_args", ("pos", ), {"kw": "kwd"}), | |
| ] | |
| self.assertEqual(events, expected_events) |
| class _SubTestCleanupHelper: | ||
| """ | ||
| Helper class to manage cleanups and context managers inside subTest blocks, | ||
| without exposing full TestCase functionality. | ||
| """ |
There was a problem hiding this comment.
Nitpick: Perhaps Context would be a better name rather than Helper:
| class _SubTestCleanupHelper: | |
| """ | |
| Helper class to manage cleanups and context managers inside subTest blocks, | |
| without exposing full TestCase functionality. | |
| """ | |
| class _SubTestContext: | |
| """ | |
| Helper class to manage cleanups and context managers inside subTest blocks, | |
| without exposing full TestCase functionality. | |
| """ |
| If successful, also adds its __exit__ method as a cleanup | ||
| function and returns the result of the __enter__ method. | ||
| """ | ||
| return _enter_context(cm, self.addCleanup) |
There was a problem hiding this comment.
Could you add a flag argument to _enter_context to prevent adding the "enterAsyncContext" suggestion?
An async version of this is probably better left to a separate PR.
| outcome = self._outcome or _Outcome() | ||
| while self._cleanups: | ||
| function, args, kwargs = self._cleanups.pop() | ||
| if hasattr(outcome, 'testPartExecutor'): |
There was a problem hiding this comment.
Is there any way the outcome would not have testPartExecutor?
| function, args, kwargs = self._cleanups.pop() | ||
| if hasattr(outcome, 'testPartExecutor'): | ||
| with outcome.testPartExecutor(self._subtest, subTest=True): | ||
| self._callCleanup(function, *args, **kwargs) |
There was a problem hiding this comment.
Can this be function(*args, **kwargs)? I see no need for an extra method.
Summary
gh-134079: Add
addCleanup,enterContextanddoCleanupstounittest.subTestand add corresponding testsDescription
This PR implements support for
addCleanup,enterContext, anddoCleanupsinunittest.subTest()contexts, as discussed in issue-134079. This enables users to register cleanups or use context managers inside a withself.subTest():block, similar to how it's done inTestCaseTests
Added tests in
test_runner.pyto validate:enterContext()behavior on success/failureI'm still learning the internals of CPython and writing tests, so please don't hesitate to suggest improvements. I'd really appreciate any feedback.