diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index 6b0282eed49566..2cba489c832976 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -524,6 +524,41 @@ Some details you should read once, but won't need to remember: TypeError: unsupported operand type(s) for +: 'int' and 'NoneType' +Skipping Tests +^^^^^^^^^^^^^^ + +You can skip all or some test examples with :exc:`SkipTest` exception. + +Let's say that you have some Python-version-specific API: + +.. doctest:: + + >>> import sys + + >>> if sys.version_info >= (3, 14): + ... def process_data(): + ... return 'processed data' + +And you want to mention it in the doctest and only run it +for the appropriate Python versions: + +.. doctest:: + + >>> if sys.version_info < (3, 14): + ... import doctest + ... raise doctest.SkipTest('This test is only for 3.14+') + + >>> # This line and below will only be executed by doctest on 3.14+ + >>> process_data() + 'processed data' + +.. exception:: SkipTest + + Special exception after raising it, all test examples will be skipped. + + .. versionadded:: 3.14 + + .. _option-flags-and-directives: .. _doctest-options: @@ -631,6 +666,8 @@ doctest decides whether actual output matches an example's expected output: The SKIP flag can also be used for temporarily "commenting out" examples. + See also: :exc:`~SkipTest`. + .. data:: COMPARISON_FLAGS diff --git a/Lib/doctest.py b/Lib/doctest.py index ea7d275c91db04..2317c121772970 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1354,6 +1354,7 @@ def __run(self, test, compileflags, out): SUCCESS, FAILURE, BOOM = range(3) # `outcome` state check = self._checker.check_output + skip_following_examples = False # Process each example. for examplenum, example in enumerate(test.examples): @@ -1374,7 +1375,8 @@ def __run(self, test, compileflags, out): self.optionflags &= ~optionflag # If 'SKIP' is set, then skip this example. - if self.optionflags & SKIP: + # Or if `SkipTest` was raised, skip all following examples. + if skip_following_examples or self.optionflags & SKIP: skips += 1 continue @@ -1400,10 +1402,16 @@ def __run(self, test, compileflags, out): raise except: exception = sys.exc_info() + exc = exception[0] + if f'{exc.__module__}.{exc.__qualname__}' == 'doctest.SkipTest': + skip_following_examples = True self.debugger.set_continue() # ==== Example Finished ==== got = self._fakeout.getvalue() # the actual output self._fakeout.truncate(0) + if skip_following_examples: + skips += 1 + continue outcome = FAILURE # guilty until proved innocent or insane # If the example executed without raising any exceptions, @@ -2230,6 +2238,25 @@ def run_docstring_examples(f, globs, verbose=False, name="NoName", for test in finder.find(f, name, globs=globs): runner.run(test, compileflags=compileflags) + +class SkipTest(Exception): + """ + Special exception that will skip all following examples. + + Can be used to conditionally skip some doctests or its parts: + + >>> import doctest + >>> 1 + 1 # will be checked + 2 + + >>> raise doctest.SkipTest("Do not check any example after this line") + + >>> 1 + 1 # won't be checked! + 3 + + """ + + ###################################################################### ## 7. Unittest Support ###################################################################### diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 3e93a3bba283c2..5fa4de6ec3e0f5 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -1950,6 +1950,22 @@ def option_directives(): r""" ValueError: line 0 of the doctest for s has an option directive on a line with no example: '# doctest: +ELLIPSIS' >>> _colorize.COLORIZE = save_colorize + +You can skip following examples via `raise SkipTest`: + + >>> def f(x): + ... r''' + ... >>> print(1) # first success + ... 1 + ... >>> raise doctest.SkipTest("All tests after this line will be skipped") + ... >>> print(4) # second skipped success + ... 4 + ... >>> print(5) # first skipped failure + ... 500 + ... ''' + >>> test = doctest.DocTestFinder().find(f)[0] + >>> doctest.DocTestRunner(verbose=False).run(test) + TestResults(failed=0, attempted=4, skipped=3) """ def test_testsource(): r""" @@ -2474,12 +2490,13 @@ def test_DocFileSuite(): >>> suite = doctest.DocFileSuite('test_doctest.txt', ... 'test_doctest4.txt', - ... 'test_doctest_skip.txt') + ... 'test_doctest_skip.txt', + ... 'test_doctest_skip2.txt') >>> result = suite.run(unittest.TestResult()) >>> result - - >>> len(result.skipped) - 1 + + >>> len(result.skipped) # not all examples in test_doctest_skip2 are skipped + 1 You can specify initial global variables: diff --git a/Lib/test/test_doctest/test_doctest_skip2.txt b/Lib/test/test_doctest/test_doctest_skip2.txt new file mode 100644 index 00000000000000..8162d216593744 --- /dev/null +++ b/Lib/test/test_doctest/test_doctest_skip2.txt @@ -0,0 +1,6 @@ +This is a sample doctest in a text file, in which all examples are skipped. + + >>> import doctest # this example is not skipped + >>> raise doctest.SkipTest("All tests after this line are skipped") + >>> 2 + 2 + 5 diff --git a/Misc/NEWS.d/next/Library/2024-08-12-14-00-18.gh-issue-117364.4d55Ex.rst b/Misc/NEWS.d/next/Library/2024-08-12-14-00-18.gh-issue-117364.4d55Ex.rst new file mode 100644 index 00000000000000..44a06c4d8cb8d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-12-14-00-18.gh-issue-117364.4d55Ex.rst @@ -0,0 +1,2 @@ +Add :exc:`doctest.SkipTest` to conditionally skip all or some doctest +examples.