Skip to content

Commit ad39f01

Browse files
gh-108885: Use subtests for doctest examples run by unittest (GH-134890)
Run each example as a subtest in unit tests synthesized by doctest.DocFileSuite() and doctest.DocTestSuite(). Add the doctest.DocTestRunner.report_skip() method.
1 parent 3c66e59 commit ad39f01

File tree

5 files changed

+279
-211
lines changed

5 files changed

+279
-211
lines changed

Doc/library/doctest.rst

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,12 +1046,15 @@ from text files and modules with doctests:
10461046
Convert doctest tests from one or more text files to a
10471047
:class:`unittest.TestSuite`.
10481048

1049-
The returned :class:`unittest.TestSuite` is to be run by the unittest framework
1050-
and runs the interactive examples in each file. If an example in any file
1051-
fails, then the synthesized unit test fails, and a :exc:`~unittest.TestCase.failureException`
1052-
exception is raised showing the name of the file containing the test and a
1053-
(sometimes approximate) line number. If all the examples in a file are
1054-
skipped, then the synthesized unit test is also marked as skipped.
1049+
The returned :class:`unittest.TestSuite` is to be run by the unittest
1050+
framework and runs the interactive examples in each file.
1051+
Each file is run as a separate unit test, and each example in a file
1052+
is run as a :ref:`subtest <subtests>`.
1053+
If any example in a file fails, then the synthesized unit test fails.
1054+
The traceback for failure or error contains the name of the file
1055+
containing the test and a (sometimes approximate) line number.
1056+
If all the examples in a file are skipped, then the synthesized unit
1057+
test is also marked as skipped.
10551058

10561059
Pass one or more paths (as strings) to text files to be examined.
10571060

@@ -1109,18 +1112,23 @@ from text files and modules with doctests:
11091112
The global ``__file__`` is added to the globals provided to doctests loaded
11101113
from a text file using :func:`DocFileSuite`.
11111114

1115+
.. versionchanged:: next
1116+
Run each example as a :ref:`subtest <subtests>`.
1117+
11121118

11131119
.. function:: DocTestSuite(module=None, globs=None, extraglobs=None, test_finder=None, setUp=None, tearDown=None, optionflags=0, checker=None)
11141120

11151121
Convert doctest tests for a module to a :class:`unittest.TestSuite`.
11161122

1117-
The returned :class:`unittest.TestSuite` is to be run by the unittest framework
1118-
and runs each doctest in the module.
1119-
Each docstring is run as a separate unit test.
1120-
If any of the doctests fail, then the synthesized unit test fails,
1121-
and a :exc:`unittest.TestCase.failureException` exception is raised
1122-
showing the name of the file containing the test and a (sometimes approximate)
1123-
line number. If all the examples in a docstring are skipped, then the
1123+
The returned :class:`unittest.TestSuite` is to be run by the unittest
1124+
framework and runs each doctest in the module.
1125+
Each docstring is run as a separate unit test, and each example in
1126+
a docstring is run as a :ref:`subtest <subtests>`.
1127+
If any of the doctests fail, then the synthesized unit test fails.
1128+
The traceback for failure or error contains the name of the file
1129+
containing the test and a (sometimes approximate) line number.
1130+
If all the examples in a docstring are skipped, then the
1131+
synthesized unit test is also marked as skipped.
11241132

11251133
Optional argument *module* provides the module to be tested. It can be a module
11261134
object or a (possibly dotted) module name. If not specified, the module calling
@@ -1145,6 +1153,9 @@ from text files and modules with doctests:
11451153
:func:`DocTestSuite` returns an empty :class:`unittest.TestSuite` if *module*
11461154
contains no docstrings instead of raising :exc:`ValueError`.
11471155

1156+
.. versionchanged:: next
1157+
Run each example as a :ref:`subtest <subtests>`.
1158+
11481159
Under the covers, :func:`DocTestSuite` creates a :class:`unittest.TestSuite` out
11491160
of :class:`!doctest.DocTestCase` instances, and :class:`!DocTestCase` is a
11501161
subclass of :class:`unittest.TestCase`. :class:`!DocTestCase` isn't documented
@@ -1507,7 +1518,7 @@ DocTestRunner objects
15071518
with strings that should be displayed. It defaults to ``sys.stdout.write``. If
15081519
capturing the output is not sufficient, then the display output can be also
15091520
customized by subclassing DocTestRunner, and overriding the methods
1510-
:meth:`report_start`, :meth:`report_success`,
1521+
:meth:`report_skip`, :meth:`report_start`, :meth:`report_success`,
15111522
:meth:`report_unexpected_exception`, and :meth:`report_failure`.
15121523

15131524
The optional keyword argument *checker* specifies the :class:`OutputChecker`
@@ -1532,6 +1543,19 @@ DocTestRunner objects
15321543
:class:`DocTestRunner` defines the following methods:
15331544

15341545

1546+
.. method:: report_skip(out, test, example)
1547+
1548+
Report that the given example was skipped. This method is provided to
1549+
allow subclasses of :class:`DocTestRunner` to customize their output; it
1550+
should not be called directly.
1551+
1552+
*example* is the example about to be processed. *test* is the test
1553+
containing *example*. *out* is the output function that was passed to
1554+
:meth:`DocTestRunner.run`.
1555+
1556+
.. versionadded:: next
1557+
1558+
15351559
.. method:: report_start(out, test, example)
15361560

15371561
Report that the test runner is about to process the given example. This method

Lib/doctest.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,14 @@ def _test():
101101
import re
102102
import sys
103103
import traceback
104+
import types
104105
import unittest
105106
from io import StringIO, IncrementalNewlineDecoder
106107
from collections import namedtuple
107108
import _colorize # Used in doctests
108109
from _colorize import ANSIColors, can_colorize
109110

110111

111-
__unittest = True
112-
113112
class TestResults(namedtuple('TestResults', 'failed attempted')):
114113
def __new__(cls, failed, attempted, *, skipped=0):
115114
results = super().__new__(cls, failed, attempted)
@@ -387,7 +386,7 @@ def __init__(self, out):
387386
self.__out = out
388387
self.__debugger_used = False
389388
# do not play signal games in the pdb
390-
pdb.Pdb.__init__(self, stdout=out, nosigint=True)
389+
super().__init__(stdout=out, nosigint=True)
391390
# still use input() to get user input
392391
self.use_rawinput = 1
393392

@@ -1280,6 +1279,11 @@ def __init__(self, checker=None, verbose=None, optionflags=0):
12801279
# Reporting methods
12811280
#/////////////////////////////////////////////////////////////////
12821281

1282+
def report_skip(self, out, test, example):
1283+
"""
1284+
Report that the given example was skipped.
1285+
"""
1286+
12831287
def report_start(self, out, test, example):
12841288
"""
12851289
Report that the test runner is about to process the given
@@ -1377,6 +1381,8 @@ def __run(self, test, compileflags, out):
13771381

13781382
# If 'SKIP' is set, then skip this example.
13791383
if self.optionflags & SKIP:
1384+
if not quiet:
1385+
self.report_skip(out, test, example)
13801386
skips += 1
13811387
continue
13821388

@@ -2274,12 +2280,63 @@ def set_unittest_reportflags(flags):
22742280
return old
22752281

22762282

2283+
class _DocTestCaseRunner(DocTestRunner):
2284+
2285+
def __init__(self, *args, test_case, test_result, **kwargs):
2286+
super().__init__(*args, **kwargs)
2287+
self._test_case = test_case
2288+
self._test_result = test_result
2289+
self._examplenum = 0
2290+
2291+
def _subTest(self):
2292+
subtest = unittest.case._SubTest(self._test_case, str(self._examplenum), {})
2293+
self._examplenum += 1
2294+
return subtest
2295+
2296+
def report_skip(self, out, test, example):
2297+
unittest.case._addSkip(self._test_result, self._subTest(), '')
2298+
2299+
def report_success(self, out, test, example, got):
2300+
self._test_result.addSubTest(self._test_case, self._subTest(), None)
2301+
2302+
def report_unexpected_exception(self, out, test, example, exc_info):
2303+
tb = self._add_traceback(exc_info[2], test, example)
2304+
exc_info = (*exc_info[:2], tb)
2305+
self._test_result.addSubTest(self._test_case, self._subTest(), exc_info)
2306+
2307+
def report_failure(self, out, test, example, got):
2308+
msg = ('Failed example:\n' + _indent(example.source) +
2309+
self._checker.output_difference(example, got, self.optionflags).rstrip('\n'))
2310+
exc = self._test_case.failureException(msg)
2311+
tb = self._add_traceback(None, test, example)
2312+
exc_info = (type(exc), exc, tb)
2313+
self._test_result.addSubTest(self._test_case, self._subTest(), exc_info)
2314+
2315+
def _add_traceback(self, traceback, test, example):
2316+
if test.lineno is None or example.lineno is None:
2317+
lineno = None
2318+
else:
2319+
lineno = test.lineno + example.lineno + 1
2320+
return types.SimpleNamespace(
2321+
tb_frame = types.SimpleNamespace(
2322+
f_globals=test.globs,
2323+
f_code=types.SimpleNamespace(
2324+
co_filename=test.filename,
2325+
co_name=test.name,
2326+
),
2327+
),
2328+
tb_next = traceback,
2329+
tb_lasti = -1,
2330+
tb_lineno = lineno,
2331+
)
2332+
2333+
22772334
class DocTestCase(unittest.TestCase):
22782335

22792336
def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
22802337
checker=None):
22812338

2282-
unittest.TestCase.__init__(self)
2339+
super().__init__()
22832340
self._dt_optionflags = optionflags
22842341
self._dt_checker = checker
22852342
self._dt_test = test
@@ -2303,30 +2360,28 @@ def tearDown(self):
23032360
test.globs.clear()
23042361
test.globs.update(self._dt_globs)
23052362

2363+
def run(self, result=None):
2364+
self._test_result = result
2365+
return super().run(result)
2366+
23062367
def runTest(self):
23072368
test = self._dt_test
2308-
old = sys.stdout
2309-
new = StringIO()
23102369
optionflags = self._dt_optionflags
2370+
result = self._test_result
23112371

23122372
if not (optionflags & REPORTING_FLAGS):
23132373
# The option flags don't include any reporting flags,
23142374
# so add the default reporting flags
23152375
optionflags |= _unittest_reportflags
2376+
if getattr(result, 'failfast', False):
2377+
optionflags |= FAIL_FAST
23162378

2317-
runner = DocTestRunner(optionflags=optionflags,
2318-
checker=self._dt_checker, verbose=False)
2319-
2320-
try:
2321-
runner.DIVIDER = "-"*70
2322-
results = runner.run(test, out=new.write, clear_globs=False)
2323-
if results.skipped == results.attempted:
2324-
raise unittest.SkipTest("all examples were skipped")
2325-
finally:
2326-
sys.stdout = old
2327-
2328-
if results.failed:
2329-
raise self.failureException(self.format_failure(new.getvalue().rstrip('\n')))
2379+
runner = _DocTestCaseRunner(optionflags=optionflags,
2380+
checker=self._dt_checker, verbose=False,
2381+
test_case=self, test_result=result)
2382+
results = runner.run(test, clear_globs=False)
2383+
if results.skipped == results.attempted:
2384+
raise unittest.SkipTest("all examples were skipped")
23302385

23312386
def format_failure(self, err):
23322387
test = self._dt_test
@@ -2441,7 +2496,7 @@ def shortDescription(self):
24412496
class SkipDocTestCase(DocTestCase):
24422497
def __init__(self, module):
24432498
self.module = module
2444-
DocTestCase.__init__(self, None)
2499+
super().__init__(None)
24452500

24462501
def setUp(self):
24472502
self.skipTest("DocTestSuite will not work with -O2 and above")

0 commit comments

Comments
 (0)