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

Skip to content

Commit 086c6b1

Browse files
bpo-45046: Support context managers in unittest (GH-28045)
Add methods enterContext() and enterClassContext() in TestCase. Add method enterAsyncContext() in IsolatedAsyncioTestCase. Add function enterModuleContext().
1 parent 8f29318 commit 086c6b1

26 files changed

Lines changed: 307 additions & 92 deletions

Doc/library/unittest.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,16 @@ Test cases
14951495
.. versionadded:: 3.1
14961496

14971497

1498+
.. method:: enterContext(cm)
1499+
1500+
Enter the supplied :term:`context manager`. If successful, also
1501+
add its :meth:`~object.__exit__` method as a cleanup function by
1502+
:meth:`addCleanup` and return the result of the
1503+
:meth:`~object.__enter__` method.
1504+
1505+
.. versionadded:: 3.11
1506+
1507+
14981508
.. method:: doCleanups()
14991509

15001510
This method is called unconditionally after :meth:`tearDown`, or
@@ -1510,6 +1520,7 @@ Test cases
15101520

15111521
.. versionadded:: 3.1
15121522

1523+
15131524
.. classmethod:: addClassCleanup(function, /, *args, **kwargs)
15141525

15151526
Add a function to be called after :meth:`tearDownClass` to cleanup
@@ -1524,6 +1535,16 @@ Test cases
15241535
.. versionadded:: 3.8
15251536

15261537

1538+
.. classmethod:: enterClassContext(cm)
1539+
1540+
Enter the supplied :term:`context manager`. If successful, also
1541+
add its :meth:`~object.__exit__` method as a cleanup function by
1542+
:meth:`addClassCleanup` and return the result of the
1543+
:meth:`~object.__enter__` method.
1544+
1545+
.. versionadded:: 3.11
1546+
1547+
15271548
.. classmethod:: doClassCleanups()
15281549

15291550
This method is called unconditionally after :meth:`tearDownClass`, or
@@ -1571,6 +1592,16 @@ Test cases
15711592

15721593
This method accepts a coroutine that can be used as a cleanup function.
15731594

1595+
.. coroutinemethod:: enterAsyncContext(cm)
1596+
1597+
Enter the supplied :term:`asynchronous context manager`. If successful,
1598+
also add its :meth:`~object.__aexit__` method as a cleanup function by
1599+
:meth:`addAsyncCleanup` and return the result of the
1600+
:meth:`~object.__aenter__` method.
1601+
1602+
.. versionadded:: 3.11
1603+
1604+
15741605
.. method:: run(result=None)
15751606

15761607
Sets up a new event loop to run the test, collecting the result into
@@ -2465,6 +2496,16 @@ To add cleanup code that must be run even in the case of an exception, use
24652496
.. versionadded:: 3.8
24662497

24672498

2499+
.. classmethod:: enterModuleContext(cm)
2500+
2501+
Enter the supplied :term:`context manager`. If successful, also
2502+
add its :meth:`~object.__exit__` method as a cleanup function by
2503+
:func:`addModuleCleanup` and return the result of the
2504+
:meth:`~object.__enter__` method.
2505+
2506+
.. versionadded:: 3.11
2507+
2508+
24682509
.. function:: doModuleCleanups()
24692510

24702511
This function is called unconditionally after :func:`tearDownModule`, or
@@ -2480,6 +2521,7 @@ To add cleanup code that must be run even in the case of an exception, use
24802521

24812522
.. versionadded:: 3.8
24822523

2524+
24832525
Signal Handling
24842526
---------------
24852527

Doc/whatsnew/3.11.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,18 @@ unicodedata
758758
* The Unicode database has been updated to version 14.0.0. (:issue:`45190`).
759759

760760

761+
unittest
762+
--------
763+
764+
* Added methods :meth:`~unittest.TestCase.enterContext` and
765+
:meth:`~unittest.TestCase.enterClassContext` of class
766+
:class:`~unittest.TestCase`, method
767+
:meth:`~unittest.IsolatedAsyncioTestCase.enterAsyncContext` of
768+
class :class:`~unittest.IsolatedAsyncioTestCase` and function
769+
:func:`unittest.enterModuleContext`.
770+
(Contributed by Serhiy Storchaka in :issue:`45046`.)
771+
772+
761773
venv
762774
----
763775

Lib/distutils/tests/test_build_ext.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ def setUp(self):
4141
# bpo-30132: On Windows, a .pdb file may be created in the current
4242
# working directory. Create a temporary working directory to cleanup
4343
# everything at the end of the test.
44-
change_cwd = os_helper.change_cwd(self.tmp_dir)
45-
change_cwd.__enter__()
46-
self.addCleanup(change_cwd.__exit__, None, None, None)
44+
self.enterContext(os_helper.change_cwd(self.tmp_dir))
4745

4846
def tearDown(self):
4947
import site

Lib/test/test__osx_support.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ def setUp(self):
1919
self.maxDiff = None
2020
self.prog_name = 'bogus_program_xxxx'
2121
self.temp_path_dir = os.path.abspath(os.getcwd())
22-
self.env = os_helper.EnvironmentVarGuard()
23-
self.addCleanup(self.env.__exit__)
22+
self.env = self.enterContext(os_helper.EnvironmentVarGuard())
2423
for cv in ('CFLAGS', 'LDFLAGS', 'CPPFLAGS',
2524
'BASECFLAGS', 'BLDSHARED', 'LDSHARED', 'CC',
2625
'CXX', 'PY_CFLAGS', 'PY_LDFLAGS', 'PY_CPPFLAGS',

Lib/test/test_argparse.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ def setUp(self):
4141
# The tests assume that line wrapping occurs at 80 columns, but this
4242
# behaviour can be overridden by setting the COLUMNS environment
4343
# variable. To ensure that this width is used, set COLUMNS to 80.
44-
env = os_helper.EnvironmentVarGuard()
44+
env = self.enterContext(os_helper.EnvironmentVarGuard())
4545
env['COLUMNS'] = '80'
46-
self.addCleanup(env.__exit__)
4746

4847

4948
class TempDirMixin(object):
@@ -3428,9 +3427,8 @@ class TestShortColumns(HelpTestCase):
34283427
but we don't want any exceptions thrown in such cases. Only ugly representation.
34293428
'''
34303429
def setUp(self):
3431-
env = os_helper.EnvironmentVarGuard()
3430+
env = self.enterContext(os_helper.EnvironmentVarGuard())
34323431
env.set("COLUMNS", '15')
3433-
self.addCleanup(env.__exit__)
34343432

34353433
parser_signature = TestHelpBiggerOptionals.parser_signature
34363434
argument_signatures = TestHelpBiggerOptionals.argument_signatures

Lib/test/test_getopt.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,10 @@
1111

1212
class GetoptTests(unittest.TestCase):
1313
def setUp(self):
14-
self.env = EnvironmentVarGuard()
14+
self.env = self.enterContext(EnvironmentVarGuard())
1515
if "POSIXLY_CORRECT" in self.env:
1616
del self.env["POSIXLY_CORRECT"]
1717

18-
def tearDown(self):
19-
self.env.__exit__()
20-
del self.env
21-
2218
def assertError(self, *args, **kwargs):
2319
self.assertRaises(getopt.GetoptError, *args, **kwargs)
2420

Lib/test/test_gettext.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117

118118
class GettextBaseTest(unittest.TestCase):
119119
def setUp(self):
120+
self.addCleanup(os_helper.rmtree, os.path.split(LOCALEDIR)[0])
120121
if not os.path.isdir(LOCALEDIR):
121122
os.makedirs(LOCALEDIR)
122123
with open(MOFILE, 'wb') as fp:
@@ -129,14 +130,10 @@ def setUp(self):
129130
fp.write(base64.decodebytes(UMO_DATA))
130131
with open(MMOFILE, 'wb') as fp:
131132
fp.write(base64.decodebytes(MMO_DATA))
132-
self.env = os_helper.EnvironmentVarGuard()
133+
self.env = self.enterContext(os_helper.EnvironmentVarGuard())
133134
self.env['LANGUAGE'] = 'xx'
134135
gettext._translations.clear()
135136

136-
def tearDown(self):
137-
self.env.__exit__()
138-
del self.env
139-
os_helper.rmtree(os.path.split(LOCALEDIR)[0])
140137

141138
GNU_MO_DATA_ISSUE_17898 = b'''\
142139
3hIElQAAAAABAAAAHAAAACQAAAAAAAAAAAAAAAAAAAAsAAAAggAAAC0AAAAAUGx1cmFsLUZvcm1z

Lib/test/test_global.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@
99
class GlobalTests(unittest.TestCase):
1010

1111
def setUp(self):
12-
self._warnings_manager = check_warnings()
13-
self._warnings_manager.__enter__()
12+
self.enterContext(check_warnings())
1413
warnings.filterwarnings("error", module="<test string>")
1514

16-
def tearDown(self):
17-
self._warnings_manager.__exit__(None, None, None)
18-
19-
2015
def test1(self):
2116
prog_text_1 = """\
2217
def wrong1():
@@ -54,9 +49,7 @@ def test4(self):
5449

5550

5651
def setUpModule():
57-
cm = warnings.catch_warnings()
58-
cm.__enter__()
59-
unittest.addModuleCleanup(cm.__exit__, None, None, None)
52+
unittest.enterModuleContext(warnings.catch_warnings())
6053
warnings.filterwarnings("error", module="<test string>")
6154

6255

Lib/test/test_importlib/source/test_finder.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -157,21 +157,12 @@ def test_dir_removal_handling(self):
157157
def test_no_read_directory(self):
158158
# Issue #16730
159159
tempdir = tempfile.TemporaryDirectory()
160+
self.enterContext(tempdir)
161+
# Since we muck with the permissions, we want to set them back to
162+
# their original values to make sure the directory can be properly
163+
# cleaned up.
160164
original_mode = os.stat(tempdir.name).st_mode
161-
def cleanup(tempdir):
162-
"""Cleanup function for the temporary directory.
163-
164-
Since we muck with the permissions, we want to set them back to
165-
their original values to make sure the directory can be properly
166-
cleaned up.
167-
168-
"""
169-
os.chmod(tempdir.name, original_mode)
170-
# If this is not explicitly called then the __del__ method is used,
171-
# but since already mucking around might as well explicitly clean
172-
# up.
173-
tempdir.__exit__(None, None, None)
174-
self.addCleanup(cleanup, tempdir)
165+
self.addCleanup(os.chmod, tempdir.name, original_mode)
175166
os.chmod(tempdir.name, stat.S_IWUSR | stat.S_IXUSR)
176167
finder = self.get_finder(tempdir.name)
177168
found = self._find(finder, 'doesnotexist')

Lib/test/test_importlib/test_namespace_pkgs.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@ def setUp(self):
6565
self.resolved_paths = [
6666
os.path.join(self.root, path) for path in self.paths
6767
]
68-
self.ctx = namespace_tree_context(path=self.resolved_paths)
69-
self.ctx.__enter__()
70-
71-
def tearDown(self):
72-
# TODO: will we ever want to pass exc_info to __exit__?
73-
self.ctx.__exit__(None, None, None)
68+
self.enterContext(namespace_tree_context(path=self.resolved_paths))
7469

7570

7671
class SingleNamespacePackage(NamespacePackageTest):

0 commit comments

Comments
 (0)