Skip to content

Commit 4f9b706

Browse files
gh-108794: doctest counts skipped tests (#108795)
* Add 'skipped' attribute to TestResults. * Add 'skips' attribute to DocTestRunner. * Rename private DocTestRunner._name2ft attribute to DocTestRunner._stats. * Use f-string for string formatting. * Add some tests. * Document DocTestRunner attributes and its API for statistics. * Document TestResults class. Co-authored-by: Alex Waygood <[email protected]>
1 parent 4ba1809 commit 4f9b706

File tree

5 files changed

+175
-67
lines changed

5 files changed

+175
-67
lines changed

Doc/library/doctest.rst

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,27 @@ DocTestParser objects
14091409
identifying this string, and is only used for error messages.
14101410

14111411

1412+
TestResults objects
1413+
^^^^^^^^^^^^^^^^^^^
1414+
1415+
1416+
.. class:: TestResults(failed, attempted)
1417+
1418+
.. attribute:: failed
1419+
1420+
Number of failed tests.
1421+
1422+
.. attribute:: attempted
1423+
1424+
Number of attempted tests.
1425+
1426+
.. attribute:: skipped
1427+
1428+
Number of skipped tests.
1429+
1430+
.. versionadded:: 3.13
1431+
1432+
14121433
.. _doctest-doctestrunner:
14131434

14141435
DocTestRunner objects
@@ -1427,7 +1448,7 @@ DocTestRunner objects
14271448
passing a subclass of :class:`OutputChecker` to the constructor.
14281449

14291450
The test runner's display output can be controlled in two ways. First, an output
1430-
function can be passed to :meth:`TestRunner.run`; this function will be called
1451+
function can be passed to :meth:`run`; this function will be called
14311452
with strings that should be displayed. It defaults to ``sys.stdout.write``. If
14321453
capturing the output is not sufficient, then the display output can be also
14331454
customized by subclassing DocTestRunner, and overriding the methods
@@ -1448,6 +1469,10 @@ DocTestRunner objects
14481469
runner compares expected output to actual output, and how it displays failures.
14491470
For more information, see section :ref:`doctest-options`.
14501471

1472+
The test runner accumulates statistics. The aggregated number of attempted,
1473+
failed and skipped examples is also available via the :attr:`tries`,
1474+
:attr:`failures` and :attr:`skips` attributes. The :meth:`run` and
1475+
:meth:`summarize` methods return a :class:`TestResults` instance.
14511476

14521477
:class:`DocTestParser` defines the following methods:
14531478

@@ -1500,7 +1525,8 @@ DocTestRunner objects
15001525
.. method:: run(test, compileflags=None, out=None, clear_globs=True)
15011526

15021527
Run the examples in *test* (a :class:`DocTest` object), and display the
1503-
results using the writer function *out*.
1528+
results using the writer function *out*. Return a :class:`TestResults`
1529+
instance.
15041530

15051531
The examples are run in the namespace ``test.globs``. If *clear_globs* is
15061532
true (the default), then this namespace will be cleared after the test runs,
@@ -1519,12 +1545,29 @@ DocTestRunner objects
15191545
.. method:: summarize(verbose=None)
15201546

15211547
Print a summary of all the test cases that have been run by this DocTestRunner,
1522-
and return a :term:`named tuple` ``TestResults(failed, attempted)``.
1548+
and return a :class:`TestResults` instance.
15231549

15241550
The optional *verbose* argument controls how detailed the summary is. If the
15251551
verbosity is not specified, then the :class:`DocTestRunner`'s verbosity is
15261552
used.
15271553

1554+
:class:`DocTestParser` has the following attributes:
1555+
1556+
.. attribute:: tries
1557+
1558+
Number of attempted examples.
1559+
1560+
.. attribute:: failures
1561+
1562+
Number of failed examples.
1563+
1564+
.. attribute:: skips
1565+
1566+
Number of skipped examples.
1567+
1568+
.. versionadded:: 3.13
1569+
1570+
15281571
.. _doctest-outputchecker:
15291572

15301573
OutputChecker objects

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ dbm
122122
from the database.
123123
(Contributed by Dong-hee Na in :gh:`107122`.)
124124

125+
doctest
126+
-------
127+
128+
* The :meth:`doctest.DocTestRunner.run` method now counts the number of skipped
129+
tests. Add :attr:`doctest.DocTestRunner.skips` and
130+
:attr:`doctest.TestResults.skipped` attributes.
131+
(Contributed by Victor Stinner in :gh:`108794`.)
132+
125133
io
126134
--
127135

Lib/doctest.py

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,23 @@ def _test():
105105
from io import StringIO, IncrementalNewlineDecoder
106106
from collections import namedtuple
107107

108-
TestResults = namedtuple('TestResults', 'failed attempted')
108+
109+
class TestResults(namedtuple('TestResults', 'failed attempted')):
110+
def __new__(cls, failed, attempted, *, skipped=0):
111+
results = super().__new__(cls, failed, attempted)
112+
results.skipped = skipped
113+
return results
114+
115+
def __repr__(self):
116+
if self.skipped:
117+
return (f'TestResults(failed={self.failed}, '
118+
f'attempted={self.attempted}, '
119+
f'skipped={self.skipped})')
120+
else:
121+
# Leave the repr() unchanged for backward compatibility
122+
# if skipped is zero
123+
return super().__repr__()
124+
109125

110126
# There are 4 basic classes:
111127
# - Example: a <source, want> pair, plus an intra-docstring line number.
@@ -1150,8 +1166,7 @@ class DocTestRunner:
11501166
"""
11511167
A class used to run DocTest test cases, and accumulate statistics.
11521168
The `run` method is used to process a single DocTest case. It
1153-
returns a tuple `(f, t)`, where `t` is the number of test cases
1154-
tried, and `f` is the number of test cases that failed.
1169+
returns a TestResults instance.
11551170
11561171
>>> tests = DocTestFinder().find(_TestClass)
11571172
>>> runner = DocTestRunner(verbose=False)
@@ -1164,8 +1179,8 @@ class DocTestRunner:
11641179
_TestClass.square -> TestResults(failed=0, attempted=1)
11651180
11661181
The `summarize` method prints a summary of all the test cases that
1167-
have been run by the runner, and returns an aggregated `(f, t)`
1168-
tuple:
1182+
have been run by the runner, and returns an aggregated TestResults
1183+
instance:
11691184
11701185
>>> runner.summarize(verbose=1)
11711186
4 items passed all tests:
@@ -1178,13 +1193,15 @@ class DocTestRunner:
11781193
Test passed.
11791194
TestResults(failed=0, attempted=7)
11801195
1181-
The aggregated number of tried examples and failed examples is
1182-
also available via the `tries` and `failures` attributes:
1196+
The aggregated number of tried examples and failed examples is also
1197+
available via the `tries`, `failures` and `skips` attributes:
11831198
11841199
>>> runner.tries
11851200
7
11861201
>>> runner.failures
11871202
0
1203+
>>> runner.skips
1204+
0
11881205
11891206
The comparison between expected outputs and actual outputs is done
11901207
by an `OutputChecker`. This comparison may be customized with a
@@ -1233,7 +1250,8 @@ def __init__(self, checker=None, verbose=None, optionflags=0):
12331250
# Keep track of the examples we've run.
12341251
self.tries = 0
12351252
self.failures = 0
1236-
self._name2ft = {}
1253+
self.skips = 0
1254+
self._stats = {}
12371255

12381256
# Create a fake output target for capturing doctest output.
12391257
self._fakeout = _SpoofOut()
@@ -1302,13 +1320,11 @@ def __run(self, test, compileflags, out):
13021320
Run the examples in `test`. Write the outcome of each example
13031321
with one of the `DocTestRunner.report_*` methods, using the
13041322
writer function `out`. `compileflags` is the set of compiler
1305-
flags that should be used to execute examples. Return a tuple
1306-
`(f, t)`, where `t` is the number of examples tried, and `f`
1307-
is the number of examples that failed. The examples are run
1308-
in the namespace `test.globs`.
1323+
flags that should be used to execute examples. Return a TestResults
1324+
instance. The examples are run in the namespace `test.globs`.
13091325
"""
1310-
# Keep track of the number of failures and tries.
1311-
failures = tries = 0
1326+
# Keep track of the number of failed, attempted, skipped examples.
1327+
failures = attempted = skips = 0
13121328

13131329
# Save the option flags (since option directives can be used
13141330
# to modify them).
@@ -1320,6 +1336,7 @@ def __run(self, test, compileflags, out):
13201336

13211337
# Process each example.
13221338
for examplenum, example in enumerate(test.examples):
1339+
attempted += 1
13231340

13241341
# If REPORT_ONLY_FIRST_FAILURE is set, then suppress
13251342
# reporting after the first failure.
@@ -1337,10 +1354,10 @@ def __run(self, test, compileflags, out):
13371354

13381355
# If 'SKIP' is set, then skip this example.
13391356
if self.optionflags & SKIP:
1357+
skips += 1
13401358
continue
13411359

13421360
# Record that we started this example.
1343-
tries += 1
13441361
if not quiet:
13451362
self.report_start(out, test, example)
13461363

@@ -1418,19 +1435,22 @@ def __run(self, test, compileflags, out):
14181435
# Restore the option flags (in case they were modified)
14191436
self.optionflags = original_optionflags
14201437

1421-
# Record and return the number of failures and tries.
1422-
self.__record_outcome(test, failures, tries)
1423-
return TestResults(failures, tries)
1438+
# Record and return the number of failures and attempted.
1439+
self.__record_outcome(test, failures, attempted, skips)
1440+
return TestResults(failures, attempted, skipped=skips)
14241441

1425-
def __record_outcome(self, test, f, t):
1442+
def __record_outcome(self, test, failures, tries, skips):
14261443
"""
1427-
Record the fact that the given DocTest (`test`) generated `f`
1428-
failures out of `t` tried examples.
1444+
Record the fact that the given DocTest (`test`) generated `failures`
1445+
failures out of `tries` tried examples.
14291446
"""
1430-
f2, t2 = self._name2ft.get(test.name, (0,0))
1431-
self._name2ft[test.name] = (f+f2, t+t2)
1432-
self.failures += f
1433-
self.tries += t
1447+
failures2, tries2, skips2 = self._stats.get(test.name, (0, 0, 0))
1448+
self._stats[test.name] = (failures + failures2,
1449+
tries + tries2,
1450+
skips + skips2)
1451+
self.failures += failures
1452+
self.tries += tries
1453+
self.skips += skips
14341454

14351455
__LINECACHE_FILENAME_RE = re.compile(r'<doctest '
14361456
r'(?P<name>.+)'
@@ -1519,9 +1539,7 @@ def out(s):
15191539
def summarize(self, verbose=None):
15201540
"""
15211541
Print a summary of all the test cases that have been run by
1522-
this DocTestRunner, and return a tuple `(f, t)`, where `f` is
1523-
the total number of failed examples, and `t` is the total
1524-
number of tried examples.
1542+
this DocTestRunner, and return a TestResults instance.
15251543
15261544
The optional `verbose` argument controls how detailed the
15271545
summary is. If the verbosity is not specified, then the
@@ -1532,59 +1550,61 @@ def summarize(self, verbose=None):
15321550
notests = []
15331551
passed = []
15341552
failed = []
1535-
totalt = totalf = 0
1536-
for x in self._name2ft.items():
1537-
name, (f, t) = x
1538-
assert f <= t
1539-
totalt += t
1540-
totalf += f
1541-
if t == 0:
1553+
total_tries = total_failures = total_skips = 0
1554+
for item in self._stats.items():
1555+
name, (failures, tries, skips) = item
1556+
assert failures <= tries
1557+
total_tries += tries
1558+
total_failures += failures
1559+
total_skips += skips
1560+
if tries == 0:
15421561
notests.append(name)
1543-
elif f == 0:
1544-
passed.append( (name, t) )
1562+
elif failures == 0:
1563+
passed.append((name, tries))
15451564
else:
1546-
failed.append(x)
1565+
failed.append(item)
15471566
if verbose:
15481567
if notests:
1549-
print(len(notests), "items had no tests:")
1568+
print(f"{len(notests)} items had no tests:")
15501569
notests.sort()
1551-
for thing in notests:
1552-
print(" ", thing)
1570+
for name in notests:
1571+
print(f" {name}")
15531572
if passed:
1554-
print(len(passed), "items passed all tests:")
1573+
print(f"{len(passed)} items passed all tests:")
15551574
passed.sort()
1556-
for thing, count in passed:
1557-
print(" %3d tests in %s" % (count, thing))
1575+
for name, count in passed:
1576+
print(f" {count:3d} tests in {name}")
15581577
if failed:
15591578
print(self.DIVIDER)
1560-
print(len(failed), "items had failures:")
1579+
print(f"{len(failed)} items had failures:")
15611580
failed.sort()
1562-
for thing, (f, t) in failed:
1563-
print(" %3d of %3d in %s" % (f, t, thing))
1581+
for name, (failures, tries, skips) in failed:
1582+
print(f" {failures:3d} of {tries:3d} in {name}")
15641583
if verbose:
1565-
print(totalt, "tests in", len(self._name2ft), "items.")
1566-
print(totalt - totalf, "passed and", totalf, "failed.")
1567-
if totalf:
1568-
print("***Test Failed***", totalf, "failures.")
1584+
print(f"{total_tries} tests in {len(self._stats)} items.")
1585+
print(f"{total_tries - total_failures} passed and {total_failures} failed.")
1586+
if total_failures:
1587+
msg = f"***Test Failed*** {total_failures} failures"
1588+
if total_skips:
1589+
msg = f"{msg} and {total_skips} skipped tests"
1590+
print(f"{msg}.")
15691591
elif verbose:
15701592
print("Test passed.")
1571-
return TestResults(totalf, totalt)
1593+
return TestResults(total_failures, total_tries, skipped=total_skips)
15721594

15731595
#/////////////////////////////////////////////////////////////////
15741596
# Backward compatibility cruft to maintain doctest.master.
15751597
#/////////////////////////////////////////////////////////////////
15761598
def merge(self, other):
1577-
d = self._name2ft
1578-
for name, (f, t) in other._name2ft.items():
1599+
d = self._stats
1600+
for name, (failures, tries, skips) in other._stats.items():
15791601
if name in d:
1580-
# Don't print here by default, since doing
1581-
# so breaks some of the buildbots
1582-
#print("*** DocTestRunner.merge: '" + name + "' in both" \
1583-
# " testers; summing outcomes.")
1584-
f2, t2 = d[name]
1585-
f = f + f2
1586-
t = t + t2
1587-
d[name] = f, t
1602+
failures2, tries2, skips2 = d[name]
1603+
failures = failures + failures2
1604+
tries = tries + tries2
1605+
skips = skips + skips2
1606+
d[name] = (failures, tries, skips)
1607+
15881608

15891609
class OutputChecker:
15901610
"""
@@ -1984,7 +2004,8 @@ class doctest.Tester, then merges the results into (or creates)
19842004
else:
19852005
master.merge(runner)
19862006

1987-
return TestResults(runner.failures, runner.tries)
2007+
return TestResults(runner.failures, runner.tries, skipped=runner.skips)
2008+
19882009

19892010
def testfile(filename, module_relative=True, name=None, package=None,
19902011
globs=None, verbose=None, report=True, optionflags=0,
@@ -2107,7 +2128,8 @@ class doctest.Tester, then merges the results into (or creates)
21072128
else:
21082129
master.merge(runner)
21092130

2110-
return TestResults(runner.failures, runner.tries)
2131+
return TestResults(runner.failures, runner.tries, skipped=runner.skips)
2132+
21112133

21122134
def run_docstring_examples(f, globs, verbose=False, name="NoName",
21132135
compileflags=None, optionflags=0):

0 commit comments

Comments
 (0)