Skip to content

Commit 975081b

Browse files
hugovkAlexWaygood
andauthored
gh-117225: Add color to doctest output (#117583)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent f6e5cc6 commit 975081b

File tree

5 files changed

+92
-15
lines changed

5 files changed

+92
-15
lines changed

Lib/doctest.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def _test():
104104
import unittest
105105
from io import StringIO, IncrementalNewlineDecoder
106106
from collections import namedtuple
107+
from traceback import _ANSIColors, _can_colorize
107108

108109

109110
class TestResults(namedtuple('TestResults', 'failed attempted')):
@@ -1179,6 +1180,9 @@ class DocTestRunner:
11791180
The `run` method is used to process a single DocTest case. It
11801181
returns a TestResults instance.
11811182
1183+
>>> save_colorize = traceback._COLORIZE
1184+
>>> traceback._COLORIZE = False
1185+
11821186
>>> tests = DocTestFinder().find(_TestClass)
11831187
>>> runner = DocTestRunner(verbose=False)
11841188
>>> tests.sort(key = lambda test: test.name)
@@ -1229,6 +1233,8 @@ class DocTestRunner:
12291233
can be also customized by subclassing DocTestRunner, and
12301234
overriding the methods `report_start`, `report_success`,
12311235
`report_unexpected_exception`, and `report_failure`.
1236+
1237+
>>> traceback._COLORIZE = save_colorize
12321238
"""
12331239
# This divider string is used to separate failure messages, and to
12341240
# separate sections of the summary.
@@ -1307,7 +1313,10 @@ def report_unexpected_exception(self, out, test, example, exc_info):
13071313
'Exception raised:\n' + _indent(_exception_traceback(exc_info)))
13081314

13091315
def _failure_header(self, test, example):
1310-
out = [self.DIVIDER]
1316+
red, reset = (
1317+
(_ANSIColors.RED, _ANSIColors.RESET) if _can_colorize() else ("", "")
1318+
)
1319+
out = [f"{red}{self.DIVIDER}{reset}"]
13111320
if test.filename:
13121321
if test.lineno is not None and example.lineno is not None:
13131322
lineno = test.lineno + example.lineno + 1
@@ -1592,6 +1601,21 @@ def summarize(self, verbose=None):
15921601
else:
15931602
failed.append((name, (failures, tries, skips)))
15941603

1604+
if _can_colorize():
1605+
bold_green = _ANSIColors.BOLD_GREEN
1606+
bold_red = _ANSIColors.BOLD_RED
1607+
green = _ANSIColors.GREEN
1608+
red = _ANSIColors.RED
1609+
reset = _ANSIColors.RESET
1610+
yellow = _ANSIColors.YELLOW
1611+
else:
1612+
bold_green = ""
1613+
bold_red = ""
1614+
green = ""
1615+
red = ""
1616+
reset = ""
1617+
yellow = ""
1618+
15951619
if verbose:
15961620
if notests:
15971621
print(f"{_n_items(notests)} had no tests:")
@@ -1600,13 +1624,13 @@ def summarize(self, verbose=None):
16001624
print(f" {name}")
16011625

16021626
if passed:
1603-
print(f"{_n_items(passed)} passed all tests:")
1627+
print(f"{green}{_n_items(passed)} passed all tests:{reset}")
16041628
for name, count in sorted(passed):
16051629
s = "" if count == 1 else "s"
1606-
print(f" {count:3d} test{s} in {name}")
1630+
print(f" {green}{count:3d} test{s} in {name}{reset}")
16071631

16081632
if failed:
1609-
print(self.DIVIDER)
1633+
print(f"{red}{self.DIVIDER}{reset}")
16101634
print(f"{_n_items(failed)} had failures:")
16111635
for name, (failures, tries, skips) in sorted(failed):
16121636
print(f" {failures:3d} of {tries:3d} in {name}")
@@ -1615,18 +1639,21 @@ def summarize(self, verbose=None):
16151639
s = "" if total_tries == 1 else "s"
16161640
print(f"{total_tries} test{s} in {_n_items(self._stats)}.")
16171641

1618-
and_f = f" and {total_failures} failed" if total_failures else ""
1619-
print(f"{total_tries - total_failures} passed{and_f}.")
1642+
and_f = (
1643+
f" and {red}{total_failures} failed{reset}"
1644+
if total_failures else ""
1645+
)
1646+
print(f"{green}{total_tries - total_failures} passed{reset}{and_f}.")
16201647

16211648
if total_failures:
16221649
s = "" if total_failures == 1 else "s"
1623-
msg = f"***Test Failed*** {total_failures} failure{s}"
1650+
msg = f"{bold_red}***Test Failed*** {total_failures} failure{s}{reset}"
16241651
if total_skips:
16251652
s = "" if total_skips == 1 else "s"
1626-
msg = f"{msg} and {total_skips} skipped test{s}"
1653+
msg = f"{msg} and {yellow}{total_skips} skipped test{s}{reset}"
16271654
print(f"{msg}.")
16281655
elif verbose:
1629-
print("Test passed.")
1656+
print(f"{bold_green}Test passed.{reset}")
16301657

16311658
return TestResults(total_failures, total_tries, skipped=total_skips)
16321659

@@ -1644,7 +1671,7 @@ def merge(self, other):
16441671
d[name] = (failures, tries, skips)
16451672

16461673

1647-
def _n_items(items: list) -> str:
1674+
def _n_items(items: list | dict) -> str:
16481675
"""
16491676
Helper to pluralise the number of items in a list.
16501677
"""
@@ -1655,7 +1682,7 @@ def _n_items(items: list) -> str:
16551682

16561683
class OutputChecker:
16571684
"""
1658-
A class used to check the whether the actual output from a doctest
1685+
A class used to check whether the actual output from a doctest
16591686
example matches the expected output. `OutputChecker` defines two
16601687
methods: `check_output`, which compares a given pair of outputs,
16611688
and returns true if they match; and `output_difference`, which

Lib/test/support/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"Error", "TestFailed", "TestDidNotRun", "ResourceDenied",
2727
# io
2828
"record_original_stdout", "get_original_stdout", "captured_stdout",
29-
"captured_stdin", "captured_stderr",
29+
"captured_stdin", "captured_stderr", "captured_output",
3030
# unittest
3131
"is_resource_enabled", "requires", "requires_freebsd_version",
3232
"requires_gil_enabled", "requires_linux_version", "requires_mac_ver",

Lib/test/test_doctest/test_doctest.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import tempfile
1717
import types
1818
import contextlib
19+
import traceback
1920

2021

2122
def doctest_skip_if(condition):
@@ -470,7 +471,7 @@ def basics(): r"""
470471
>>> tests = finder.find(sample_func)
471472
472473
>>> print(tests) # doctest: +ELLIPSIS
473-
[<DocTest sample_func from test_doctest.py:37 (1 example)>]
474+
[<DocTest sample_func from test_doctest.py:38 (1 example)>]
474475
475476
The exact name depends on how test_doctest was invoked, so allow for
476477
leading path components.
@@ -892,6 +893,9 @@ def basics(): r"""
892893
DocTestRunner is used to run DocTest test cases, and to accumulate
893894
statistics. Here's a simple DocTest case we can use:
894895
896+
>>> save_colorize = traceback._COLORIZE
897+
>>> traceback._COLORIZE = False
898+
895899
>>> def f(x):
896900
... '''
897901
... >>> x = 12
@@ -946,6 +950,8 @@ def basics(): r"""
946950
6
947951
ok
948952
TestResults(failed=1, attempted=3)
953+
954+
>>> traceback._COLORIZE = save_colorize
949955
"""
950956
def verbose_flag(): r"""
951957
The `verbose` flag makes the test runner generate more detailed
@@ -1021,6 +1027,9 @@ def exceptions(): r"""
10211027
lines between the first line and the type/value may be omitted or
10221028
replaced with any other string:
10231029
1030+
>>> save_colorize = traceback._COLORIZE
1031+
>>> traceback._COLORIZE = False
1032+
10241033
>>> def f(x):
10251034
... '''
10261035
... >>> x = 12
@@ -1251,6 +1260,8 @@ def exceptions(): r"""
12511260
...
12521261
ZeroDivisionError: integer division or modulo by zero
12531262
TestResults(failed=1, attempted=1)
1263+
1264+
>>> traceback._COLORIZE = save_colorize
12541265
"""
12551266
def displayhook(): r"""
12561267
Test that changing sys.displayhook doesn't matter for doctest.
@@ -1292,6 +1303,9 @@ def optionflags(): r"""
12921303
The DONT_ACCEPT_TRUE_FOR_1 flag disables matches between True/False
12931304
and 1/0:
12941305
1306+
>>> save_colorize = traceback._COLORIZE
1307+
>>> traceback._COLORIZE = False
1308+
12951309
>>> def f(x):
12961310
... '>>> True\n1\n'
12971311
@@ -1711,6 +1725,7 @@ def optionflags(): r"""
17111725
17121726
Clean up.
17131727
>>> del doctest.OPTIONFLAGS_BY_NAME[unlikely]
1728+
>>> traceback._COLORIZE = save_colorize
17141729
17151730
"""
17161731

@@ -1721,6 +1736,9 @@ def option_directives(): r"""
17211736
single example. To turn an option on for an example, follow that
17221737
example with a comment of the form ``# doctest: +OPTION``:
17231738
1739+
>>> save_colorize = traceback._COLORIZE
1740+
>>> traceback._COLORIZE = False
1741+
17241742
>>> def f(x): r'''
17251743
... >>> print(list(range(10))) # should fail: no ellipsis
17261744
... [0, 1, ..., 9]
@@ -1928,6 +1946,8 @@ def option_directives(): r"""
19281946
>>> test = doctest.DocTestParser().get_doctest(s, {}, 's', 's.py', 0)
19291947
Traceback (most recent call last):
19301948
ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS'
1949+
1950+
>>> traceback._COLORIZE = save_colorize
19311951
"""
19321952

19331953
def test_testsource(): r"""
@@ -2011,6 +2031,9 @@ def test_pdb_set_trace():
20112031
with a version that restores stdout. This is necessary for you to
20122032
see debugger output.
20132033
2034+
>>> save_colorize = traceback._COLORIZE
2035+
>>> traceback._COLORIZE = False
2036+
20142037
>>> doc = '''
20152038
... >>> x = 42
20162039
... >>> raise Exception('clé')
@@ -2065,7 +2088,7 @@ def test_pdb_set_trace():
20652088
... finally:
20662089
... sys.stdin = real_stdin
20672090
--Return--
2068-
> <doctest test.test_doctest.test_doctest.test_pdb_set_trace[7]>(3)calls_set_trace()->None
2091+
> <doctest test.test_doctest.test_doctest.test_pdb_set_trace[9]>(3)calls_set_trace()->None
20692092
-> import pdb; pdb.set_trace()
20702093
(Pdb) print(y)
20712094
2
@@ -2133,6 +2156,8 @@ def test_pdb_set_trace():
21332156
Got:
21342157
9
21352158
TestResults(failed=1, attempted=3)
2159+
2160+
>>> traceback._COLORIZE = save_colorize
21362161
"""
21372162

21382163
def test_pdb_set_trace_nested():
@@ -2667,7 +2692,10 @@ def test_testfile(): r"""
26672692
called with the name of a file, which is taken to be relative to the
26682693
calling module. The return value is (#failures, #tests).
26692694
2670-
We don't want `-v` in sys.argv for these tests.
2695+
We don't want color or `-v` in sys.argv for these tests.
2696+
2697+
>>> save_colorize = traceback._COLORIZE
2698+
>>> traceback._COLORIZE = False
26712699
26722700
>>> save_argv = sys.argv
26732701
>>> if '-v' in sys.argv:
@@ -2835,6 +2863,7 @@ def test_testfile(): r"""
28352863
TestResults(failed=0, attempted=2)
28362864
>>> doctest.master = None # Reset master.
28372865
>>> sys.argv = save_argv
2866+
>>> traceback._COLORIZE = save_colorize
28382867
"""
28392868

28402869
class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader):
@@ -2972,6 +3001,9 @@ def test_testmod(): r"""
29723001
def test_unicode(): """
29733002
Check doctest with a non-ascii filename:
29743003
3004+
>>> save_colorize = traceback._COLORIZE
3005+
>>> traceback._COLORIZE = False
3006+
29753007
>>> doc = '''
29763008
... >>> raise Exception('clé')
29773009
... '''
@@ -2997,8 +3029,11 @@ def test_unicode(): """
29973029
raise Exception('clé')
29983030
Exception: clé
29993031
TestResults(failed=1, attempted=1)
3032+
3033+
>>> traceback._COLORIZE = save_colorize
30003034
"""
30013035

3036+
30023037
@doctest_skip_if(not support.has_subprocess_support)
30033038
def test_CLI(): r"""
30043039
The doctest module can be used to run doctests against an arbitrary file.
@@ -3290,6 +3325,9 @@ def test_run_doctestsuite_multiple_times():
32903325

32913326
def test_exception_with_note(note):
32923327
"""
3328+
>>> save_colorize = traceback._COLORIZE
3329+
>>> traceback._COLORIZE = False
3330+
32933331
>>> test_exception_with_note('Note')
32943332
Traceback (most recent call last):
32953333
...
@@ -3339,6 +3377,8 @@ def test_exception_with_note(note):
33393377
ValueError: message
33403378
note
33413379
TestResults(failed=1, attempted=...)
3380+
3381+
>>> traceback._COLORIZE = save_colorize
33423382
"""
33433383
exc = ValueError('Text')
33443384
exc.add_note(note)
@@ -3419,6 +3459,9 @@ def test_syntax_error_subclass_from_stdlib():
34193459

34203460
def test_syntax_error_with_incorrect_expected_note():
34213461
"""
3462+
>>> save_colorize = traceback._COLORIZE
3463+
>>> traceback._COLORIZE = False
3464+
34223465
>>> def f(x):
34233466
... r'''
34243467
... >>> exc = SyntaxError("error", ("x.py", 23, None, "bad syntax"))
@@ -3447,6 +3490,8 @@ def test_syntax_error_with_incorrect_expected_note():
34473490
note1
34483491
note2
34493492
TestResults(failed=1, attempted=...)
3493+
3494+
>>> traceback._COLORIZE = save_colorize
34503495
"""
34513496

34523497

Lib/traceback.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,12 @@ class _ANSIColors:
448448
BOLD_RED = '\x1b[1;31m'
449449
MAGENTA = '\x1b[35m'
450450
BOLD_MAGENTA = '\x1b[1;35m'
451+
GREEN = "\x1b[32m"
452+
BOLD_GREEN = "\x1b[1;32m"
451453
GREY = '\x1b[90m'
452454
RESET = '\x1b[0m'
455+
YELLOW = "\x1b[33m"
456+
453457

454458
class StackSummary(list):
455459
"""A list of FrameSummary objects, representing a stack of frames."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add colour to doctest output. Patch by Hugo van Kemenade.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy