Skip to content

Commit 7dc6aa0

Browse files
hugovkEclips4
authored andcommitted
pythongh-127221: Add colour to unittest output (python#127223)
Co-authored-by: Kirill Podoprigora <[email protected]>
1 parent 9914861 commit 7dc6aa0

15 files changed

+136
-60
lines changed

Doc/conf.py

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@
7878
.. |python_version_literal| replace:: ``Python {version}``
7979
.. |python_x_dot_y_literal| replace:: ``python{version}``
8080
.. |usr_local_bin_python_x_dot_y_literal| replace:: ``/usr/local/bin/python{version}``
81+
82+
.. Apparently this how you hack together a formatted link:
83+
(https://www.docutils.org/docs/ref/rst/directives.html#replacement-text)
84+
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
85+
.. _FORCE_COLOR: https://force-color.org/
86+
.. |NO_COLOR| replace:: ``NO_COLOR``
87+
.. _NO_COLOR: https://no-color.org/
8188
"""
8289

8390
# There are two options for replacing |today|. Either, you set today to some

Doc/library/doctest.rst

+4
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ examples of doctests in the standard Python test suite and libraries.
136136
Especially useful examples can be found in the standard test file
137137
:file:`Lib/test/test_doctest/test_doctest.py`.
138138

139+
.. versionadded:: 3.13
140+
Output is colorized by default and can be
141+
:ref:`controlled using environment variables <using-on-controlling-color>`.
142+
139143

140144
.. _doctest-simple-testmod:
141145

Doc/library/traceback.rst

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ The module's API can be divided into two parts:
4444
necessary for later formatting without holding references to actual exception
4545
and traceback objects.
4646

47+
.. versionadded:: 3.13
48+
Output is colorized by default and can be
49+
:ref:`controlled using environment variables <using-on-controlling-color>`.
50+
4751

4852
Module-Level Functions
4953
----------------------

Doc/library/unittest.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ test runner
4646
a textual interface, or return a special value to indicate the results of
4747
executing the tests.
4848

49-
5049
.. seealso::
5150

5251
Module :mod:`doctest`
@@ -198,6 +197,9 @@ For a list of all the command-line options::
198197
In earlier versions it was only possible to run individual test methods and
199198
not modules or classes.
200199

200+
.. versionadded:: 3.14
201+
Output is colorized by default and can be
202+
:ref:`controlled using environment variables <using-on-controlling-color>`.
201203

202204
Command-line options
203205
~~~~~~~~~~~~~~~~~~~~

Doc/using/cmdline.rst

-8
Original file line numberDiff line numberDiff line change
@@ -663,14 +663,6 @@ output. To control the color output only in the Python interpreter, the
663663
precedence over ``NO_COLOR``, which in turn takes precedence over
664664
``FORCE_COLOR``.
665665

666-
.. Apparently this how you hack together a formatted link:
667-
668-
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
669-
.. _FORCE_COLOR: https://force-color.org/
670-
671-
.. |NO_COLOR| replace:: ``NO_COLOR``
672-
.. _NO_COLOR: https://no-color.org/
673-
674666
Options you shouldn't use
675667
~~~~~~~~~~~~~~~~~~~~~~~~~
676668

Doc/whatsnew/3.13.rst

-9
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,6 @@ Improved error messages
252252
the canonical |NO_COLOR|_ and |FORCE_COLOR|_ environment variables.
253253
(Contributed by Pablo Galindo Salgado in :gh:`112730`.)
254254

255-
.. Apparently this how you hack together a formatted link:
256-
(https://www.docutils.org/docs/ref/rst/directives.html#replacement-text)
257-
258-
.. |FORCE_COLOR| replace:: ``FORCE_COLOR``
259-
.. _FORCE_COLOR: https://force-color.org/
260-
261-
.. |NO_COLOR| replace:: ``NO_COLOR``
262-
.. _NO_COLOR: https://no-color.org/
263-
264255
* A common mistake is to write a script with the same name as a
265256
standard library module. When this results in errors, we now
266257
display a more helpful error message:

Doc/whatsnew/3.14.rst

+7
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,13 @@ unicodedata
616616
unittest
617617
--------
618618

619+
* :mod:`unittest` output is now colored by default.
620+
This can be controlled via the :envvar:`PYTHON_COLORS` environment
621+
variable as well as the canonical |NO_COLOR|_
622+
and |FORCE_COLOR|_ environment variables.
623+
See also :ref:`using-on-controlling-color`.
624+
(Contributed by Hugo van Kemenade in :gh:`127221`.)
625+
619626
* unittest discovery supports :term:`namespace package` as start
620627
directory again. It was removed in Python 3.11.
621628
(Contributed by Jacob Walls in :gh:`80958`.)

Lib/test/test_unittest/test_async_case.py

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import contextvars
33
import unittest
44
from test import support
5+
from test.support import force_not_colorized
56

67
support.requires_working_socket(module=True)
78

@@ -252,6 +253,7 @@ async def on_cleanup(self):
252253
test.doCleanups()
253254
self.assertEqual(events, ['asyncSetUp', 'test', 'asyncTearDown', 'cleanup'])
254255

256+
@force_not_colorized
255257
def test_exception_in_tear_clean_up(self):
256258
class Test(unittest.IsolatedAsyncioTestCase):
257259
async def asyncSetUp(self):

Lib/test/test_unittest/test_program.py

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from test import support
55
import unittest
66
import test.test_unittest
7+
from test.support import force_not_colorized
78
from test.test_unittest.test_result import BufferedWriter
89

910

@@ -120,6 +121,7 @@ def run(self, test):
120121
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
121122
program.testNames)
122123

124+
@force_not_colorized
123125
def test_NonExit(self):
124126
stream = BufferedWriter()
125127
program = unittest.main(exit=False,
@@ -135,6 +137,7 @@ def test_NonExit(self):
135137
'expected failures=1, unexpected successes=1)\n')
136138
self.assertTrue(out.endswith(expected))
137139

140+
@force_not_colorized
138141
def test_Exit(self):
139142
stream = BufferedWriter()
140143
with self.assertRaises(SystemExit) as cm:
@@ -152,6 +155,7 @@ def test_Exit(self):
152155
'expected failures=1, unexpected successes=1)\n')
153156
self.assertTrue(out.endswith(expected))
154157

158+
@force_not_colorized
155159
def test_ExitAsDefault(self):
156160
stream = BufferedWriter()
157161
with self.assertRaises(SystemExit):
@@ -167,6 +171,7 @@ def test_ExitAsDefault(self):
167171
'expected failures=1, unexpected successes=1)\n')
168172
self.assertTrue(out.endswith(expected))
169173

174+
@force_not_colorized
170175
def test_ExitSkippedSuite(self):
171176
stream = BufferedWriter()
172177
with self.assertRaises(SystemExit) as cm:
@@ -179,6 +184,7 @@ def test_ExitSkippedSuite(self):
179184
expected = '\n\nOK (skipped=1)\n'
180185
self.assertTrue(out.endswith(expected))
181186

187+
@force_not_colorized
182188
def test_ExitEmptySuite(self):
183189
stream = BufferedWriter()
184190
with self.assertRaises(SystemExit) as cm:

Lib/test/test_unittest/test_result.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import traceback
88
import unittest
99
from unittest.util import strclass
10+
from test.support import force_not_colorized
1011
from test.test_unittest.support import BufferedWriter
1112

1213

1314
class MockTraceback(object):
1415
class TracebackException:
1516
def __init__(self, *args, **kwargs):
1617
self.capture_locals = kwargs.get('capture_locals', False)
17-
def format(self):
18+
def format(self, **kwargs):
1819
result = ['A traceback']
1920
if self.capture_locals:
2021
result.append('locals')
@@ -205,6 +206,7 @@ def test_1(self):
205206
self.assertIs(test_case, test)
206207
self.assertIsInstance(formatted_exc, str)
207208

209+
@force_not_colorized
208210
def test_addFailure_filter_traceback_frames(self):
209211
class Foo(unittest.TestCase):
210212
def test_1(self):
@@ -231,6 +233,7 @@ def get_exc_info():
231233
self.assertEqual(len(dropped), 1)
232234
self.assertIn("raise self.failureException(msg)", dropped[0])
233235

236+
@force_not_colorized
234237
def test_addFailure_filter_traceback_frames_context(self):
235238
class Foo(unittest.TestCase):
236239
def test_1(self):
@@ -260,6 +263,7 @@ def get_exc_info():
260263
self.assertEqual(len(dropped), 1)
261264
self.assertIn("raise self.failureException(msg)", dropped[0])
262265

266+
@force_not_colorized
263267
def test_addFailure_filter_traceback_frames_chained_exception_self_loop(self):
264268
class Foo(unittest.TestCase):
265269
def test_1(self):
@@ -285,6 +289,7 @@ def get_exc_info():
285289
formatted_exc = result.failures[0][1]
286290
self.assertEqual(formatted_exc.count("Exception: Loop\n"), 1)
287291

292+
@force_not_colorized
288293
def test_addFailure_filter_traceback_frames_chained_exception_cycle(self):
289294
class Foo(unittest.TestCase):
290295
def test_1(self):
@@ -446,6 +451,7 @@ def testFailFast(self):
446451
result.addUnexpectedSuccess(None)
447452
self.assertTrue(result.shouldStop)
448453

454+
@force_not_colorized
449455
def testFailFastSetByRunner(self):
450456
stream = BufferedWriter()
451457
runner = unittest.TextTestRunner(stream=stream, failfast=True)
@@ -619,6 +625,7 @@ def _run_test(self, test_name, verbosity, tearDownError=None):
619625
test.run(result)
620626
return stream.getvalue()
621627

628+
@force_not_colorized
622629
def testDotsOutput(self):
623630
self.assertEqual(self._run_test('testSuccess', 1), '.')
624631
self.assertEqual(self._run_test('testSkip', 1), 's')
@@ -627,6 +634,7 @@ def testDotsOutput(self):
627634
self.assertEqual(self._run_test('testExpectedFailure', 1), 'x')
628635
self.assertEqual(self._run_test('testUnexpectedSuccess', 1), 'u')
629636

637+
@force_not_colorized
630638
def testLongOutput(self):
631639
classname = f'{__name__}.{self.Test.__qualname__}'
632640
self.assertEqual(self._run_test('testSuccess', 2),
@@ -642,17 +650,21 @@ def testLongOutput(self):
642650
self.assertEqual(self._run_test('testUnexpectedSuccess', 2),
643651
f'testUnexpectedSuccess ({classname}.testUnexpectedSuccess) ... unexpected success\n')
644652

653+
@force_not_colorized
645654
def testDotsOutputSubTestSuccess(self):
646655
self.assertEqual(self._run_test('testSubTestSuccess', 1), '.')
647656

657+
@force_not_colorized
648658
def testLongOutputSubTestSuccess(self):
649659
classname = f'{__name__}.{self.Test.__qualname__}'
650660
self.assertEqual(self._run_test('testSubTestSuccess', 2),
651661
f'testSubTestSuccess ({classname}.testSubTestSuccess) ... ok\n')
652662

663+
@force_not_colorized
653664
def testDotsOutputSubTestMixed(self):
654665
self.assertEqual(self._run_test('testSubTestMixed', 1), 'sFE')
655666

667+
@force_not_colorized
656668
def testLongOutputSubTestMixed(self):
657669
classname = f'{__name__}.{self.Test.__qualname__}'
658670
self.assertEqual(self._run_test('testSubTestMixed', 2),
@@ -661,6 +673,7 @@ def testLongOutputSubTestMixed(self):
661673
f' testSubTestMixed ({classname}.testSubTestMixed) [fail] (c=3) ... FAIL\n'
662674
f' testSubTestMixed ({classname}.testSubTestMixed) [error] (d=4) ... ERROR\n')
663675

676+
@force_not_colorized
664677
def testDotsOutputTearDownFail(self):
665678
out = self._run_test('testSuccess', 1, AssertionError('fail'))
666679
self.assertEqual(out, 'F')
@@ -671,6 +684,7 @@ def testDotsOutputTearDownFail(self):
671684
out = self._run_test('testSkip', 1, AssertionError('fail'))
672685
self.assertEqual(out, 'sF')
673686

687+
@force_not_colorized
674688
def testLongOutputTearDownFail(self):
675689
classname = f'{__name__}.{self.Test.__qualname__}'
676690
out = self._run_test('testSuccess', 2, AssertionError('fail'))

Lib/test/test_unittest/test_runner.py

+13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pickle
55
import subprocess
66
from test import support
7+
from test.support import force_not_colorized
78

89
import unittest
910
from unittest.case import _Outcome
@@ -106,6 +107,7 @@ def cleanup2(*args, **kwargs):
106107
self.assertTrue(test.doCleanups())
107108
self.assertEqual(cleanups, [(2, (), {}), (1, (1, 2, 3), dict(four='hello', five='goodbye'))])
108109

110+
@force_not_colorized
109111
def testCleanUpWithErrors(self):
110112
class TestableTest(unittest.TestCase):
111113
def testNothing(self):
@@ -416,6 +418,7 @@ def cleanup2():
416418
self.assertIsInstance(e2[1], CustomError)
417419
self.assertEqual(str(e2[1]), 'cleanup1')
418420

421+
@force_not_colorized
419422
def test_with_errors_addCleanUp(self):
420423
ordering = []
421424
class TestableTest(unittest.TestCase):
@@ -439,6 +442,7 @@ def tearDownClass(cls):
439442
['setUpClass', 'setUp', 'cleanup_exc',
440443
'tearDownClass', 'cleanup_good'])
441444

445+
@force_not_colorized
442446
def test_run_with_errors_addClassCleanUp(self):
443447
ordering = []
444448
class TestableTest(unittest.TestCase):
@@ -462,6 +466,7 @@ def tearDownClass(cls):
462466
['setUpClass', 'setUp', 'test', 'cleanup_good',
463467
'tearDownClass', 'cleanup_exc'])
464468

469+
@force_not_colorized
465470
def test_with_errors_in_addClassCleanup_and_setUps(self):
466471
ordering = []
467472
class_blow_up = False
@@ -514,6 +519,7 @@ def tearDownClass(cls):
514519
['setUpClass', 'setUp', 'tearDownClass',
515520
'cleanup_exc'])
516521

522+
@force_not_colorized
517523
def test_with_errors_in_tearDownClass(self):
518524
ordering = []
519525
class TestableTest(unittest.TestCase):
@@ -590,6 +596,7 @@ def test(self):
590596
'inner setup', 'inner test', 'inner cleanup',
591597
'end outer test', 'outer cleanup'])
592598

599+
@force_not_colorized
593600
def test_run_empty_suite_error_message(self):
594601
class EmptyTest(unittest.TestCase):
595602
pass
@@ -663,6 +670,7 @@ class Module(object):
663670
self.assertEqual(cleanups,
664671
[((1, 2), {'function': 'hello'})])
665672

673+
@force_not_colorized
666674
def test_run_module_cleanUp(self):
667675
blowUp = True
668676
ordering = []
@@ -802,6 +810,7 @@ def tearDownClass(cls):
802810
'tearDownClass', 'cleanup_good'])
803811
self.assertEqual(unittest.case._module_cleanups, [])
804812

813+
@force_not_colorized
805814
def test_run_module_cleanUp_when_teardown_exception(self):
806815
ordering = []
807816
class Module(object):
@@ -963,6 +972,7 @@ def testNothing(self):
963972
self.assertEqual(cleanups,
964973
[((1, 2), {'function': 3, 'self': 4})])
965974

975+
@force_not_colorized
966976
def test_with_errors_in_addClassCleanup(self):
967977
ordering = []
968978

@@ -996,6 +1006,7 @@ def tearDownClass(cls):
9961006
['setUpModule', 'setUpClass', 'test', 'tearDownClass',
9971007
'cleanup_exc', 'tearDownModule', 'cleanup_good'])
9981008

1009+
@force_not_colorized
9991010
def test_with_errors_in_addCleanup(self):
10001011
ordering = []
10011012
class Module(object):
@@ -1026,6 +1037,7 @@ def tearDown(self):
10261037
['setUpModule', 'setUp', 'test', 'tearDown',
10271038
'cleanup_exc', 'tearDownModule', 'cleanup_good'])
10281039

1040+
@force_not_colorized
10291041
def test_with_errors_in_addModuleCleanup_and_setUps(self):
10301042
ordering = []
10311043
module_blow_up = False
@@ -1318,6 +1330,7 @@ def MockResultClass(*args):
13181330
expectedresult = (runner.stream, DESCRIPTIONS, VERBOSITY)
13191331
self.assertEqual(runner._makeResult(), expectedresult)
13201332

1333+
@force_not_colorized
13211334
@support.requires_subprocess()
13221335
def test_warnings(self):
13231336
"""

0 commit comments

Comments
 (0)