-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-134079: Add addCleanup
, enterContext
and doCleanups
to unittest.subTest
and tests
#134318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @encukou
Lib/unittest/case.py
Outdated
@@ -1626,3 +1629,46 @@ def shortDescription(self): | |||
|
|||
def __str__(self): | |||
return "{} {}".format(self.test_case, self._subDescription()) | |||
|
|||
|
|||
class _SubTestCleanupHelper(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this one i don't see any difference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put the tearDown() after the setUp() instead? it'll be easier to maintain and read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This looks great so far. Do you want to write the documentation as well?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be function(*args, **kwargs)
? I see no need for an extra method.
Summary
gh-134079: Add
addCleanup
,enterContext
anddoCleanups
tounittest.subTest
and add corresponding testsDescription
This PR implements support for
addCleanup
,enterContext
, anddoCleanups
inunittest.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 inTestCase
Tests
Added tests in
test_runner.py
to 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.