Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ArunRawat404
Copy link

Summary

gh-134079: Add addCleanup, enterContext and doCleanups to unittest.subTest and add corresponding tests

Description

This PR implements support for addCleanup, enterContext, and doCleanups in unittest.subTest() contexts, as discussed in issue-134079. This enables users to register cleanups or use context managers inside a with self.subTest(): block, similar to how it's done in TestCase

Tests

Added tests in test_runner.py to validate:

  • Cleanup order (LIFO)
  • Cleanup on failure
  • enterContext() behavior on success/failure
  • Exception safety in context manager entry and exit

I'm still learning the internals of CPython and writing tests, so please don't hesitate to suggest improvements. I'd really appreciate any feedback.

@python-cla-bot
Copy link

python-cla-bot bot commented May 20, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

bedevere-app bot commented May 20, 2025

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 skip news label instead.

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -1626,3 +1629,46 @@ def shortDescription(self):

def __str__(self):
return "{} {}".format(self.test_case, self._subDescription())


class _SubTestCleanupHelper():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class _SubTestCleanupHelper():
class _SubTestCleanupHelper:

Comment on lines 1645 to 1646
def _callCleanup(self, function, /, *args, **kwargs):
function(*args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines 1652 to 1654
if hasattr(TestSubTestCleanups, '_active_instance'):
del TestSubTestCleanups._active_instance

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if hasattr(TestSubTestCleanups, '_active_instance'):
del TestSubTestCleanups._active_instance
if hasattr(TestSubTestCleanups, '_active_instance'):
del TestSubTestCleanups._active_instance

Copy link
Author

@ArunRawat404 ArunRawat404 May 21, 2025

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

Copy link
Member

@picnixz picnixz May 22, 2025

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):
Copy link
Member

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.

Copy link
Member

@encukou encukou left a 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?

Comment on lines +1503 to +1522
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)
Copy link
Member

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.

Suggested change
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)

Comment on lines +1634 to +1638
class _SubTestCleanupHelper:
"""
Helper class to manage cleanups and context managers inside subTest blocks,
without exposing full TestCase functionality.
"""
Copy link
Member

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:

Suggested change
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)
Copy link
Member

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'):
Copy link
Member

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)
Copy link
Member

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.

@serhiy-storchaka serhiy-storchaka self-requested a review June 4, 2025 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants