diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index 02b73ccd3f3d19..c7ef65ed557573 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -616,7 +616,60 @@ doctest decides whether actual output matches an example's expected output: sequence of whitespace within the actual output. By default, whitespace must match exactly. :const:`NORMALIZE_WHITESPACE` is especially useful when a line of expected output is very long, and you want to wrap it across multiple lines in - your source. + your source. If the expected output does not contain any whitespace, consider + using :data:`IGNORE_LINEBREAK` or :data:`ELLIPSIS`. + + +.. data:: IGNORE_LINEBREAK + + When specified, single line breaks in the expected output are eliminated, + thereby allowing strings without whitespaces to span multiple lines. + + .. doctest:: + :no-trim-doctest-flags: + + >>> "foobar123456" # doctest: +IGNORE_LINEBREAK + 'foobar + 123456' + + Consider using :data:`NORMALIZE_WHITESPACE` when strings with whitespaces + need to be split across multiple lines: + + .. doctest:: + :no-trim-doctest-flags: + + >>> "the string to split" # doctest: +NORMALIZE_WHITESPACE + 'the string + to split' + + Note that any leading whitespaces on each expected output line are retained. + In other words, the following expected outputs are equivalent under + :data:`!IGNORE_LINEBREAK`: + + .. code-block:: + + [ + 'a', 'b', 'c', + '1', '2', '3' + ] + + [ 'a', 'b', 'c', '1', '2', '3'] + + To break a list-like output with :data:`!IGNORE_LINEBREAK`, + leading whitespaces for visual indentation purposes should + be avoided, for instance: + + .. doctest:: + :no-trim-doctest-flags: + + >>> list("abc123") # doctest: +IGNORE_LINEBREAK + ['a', 'b', 'c', + '1', '2', '3'] + + For more complex outputs, consider using :func:`pprint.pp` and matching + its output directly. + + .. versionadded:: next .. index:: single: ...; in doctests diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 54a7d0f3c57dad..3e7854add0b202 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -311,6 +311,23 @@ difflib (Contributed by Jiahao Li in :gh:`134580`.) +doctest +------- + +* Add :data:`~doctest.IGNORE_LINEBREAK` option to allow breaking expected + output strings without whitespaces into multiple lines: + + .. doctest:: + :no-trim-doctest-flags: + + >>> import string + >>> print(string.ascii_letters) # doctest: +IGNORE_LINEBREAK + abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + + (Contributed by Bénédikt Tran in :gh:`138135`.) + + hashlib ------- diff --git a/Lib/doctest.py b/Lib/doctest.py index 92a2ab4f7e66f8..ad78f014ed3b91 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -53,6 +53,7 @@ def _test(): 'DONT_ACCEPT_TRUE_FOR_1', 'DONT_ACCEPT_BLANKLINE', 'NORMALIZE_WHITESPACE', + 'IGNORE_LINEBREAK', 'ELLIPSIS', 'SKIP', 'IGNORE_EXCEPTION_DETAIL', @@ -156,6 +157,7 @@ def register_optionflag(name): DONT_ACCEPT_TRUE_FOR_1 = register_optionflag('DONT_ACCEPT_TRUE_FOR_1') DONT_ACCEPT_BLANKLINE = register_optionflag('DONT_ACCEPT_BLANKLINE') NORMALIZE_WHITESPACE = register_optionflag('NORMALIZE_WHITESPACE') +IGNORE_LINEBREAK = register_optionflag('IGNORE_LINEBREAK') ELLIPSIS = register_optionflag('ELLIPSIS') SKIP = register_optionflag('SKIP') IGNORE_EXCEPTION_DETAIL = register_optionflag('IGNORE_EXCEPTION_DETAIL') @@ -1751,9 +1753,18 @@ def check_output(self, want, got, optionflags): if got == want: return True + # This flag causes doctest to ignore '\n' in `want`. + # Note that this can be used in conjunction with + # the NORMALIZE_WHITESPACE and ELLIPSIS flags. + if optionflags & IGNORE_LINEBREAK: + # `want` originally ends with '\n' so we add it back + want = ''.join(want.splitlines()) + '\n' + if got == want: + return True + # This flag causes doctest to ignore any differences in the # contents of whitespace strings. Note that this can be used - # in conjunction with the ELLIPSIS flag. + # in conjunction with the IGNORE_LINEBREAK and ELLIPSIS flags. if optionflags & NORMALIZE_WHITESPACE: got = ' '.join(got.split()) want = ' '.join(want.split()) @@ -2268,7 +2279,7 @@ def set_unittest_reportflags(flags): >>> doctest.set_unittest_reportflags(ELLIPSIS) Traceback (most recent call last): ... - ValueError: ('Only reporting flags allowed', 8) + ValueError: ('Only reporting flags allowed', 16) >>> doctest.set_unittest_reportflags(old) == (REPORT_NDIFF | ... REPORT_ONLY_FIRST_FAILURE) @@ -2924,6 +2935,13 @@ def get(self): 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] """, + + "line break elimination": r""" + >>> "foobar" # doctest: +IGNORE_LINEBREAK + 'foo + bar + ' + """, } diff --git a/Lib/test/test_doctest/test_doctest.py b/Lib/test/test_doctest/test_doctest.py index 0fa74407e3c436..cdd8c7b829d90d 100644 --- a/Lib/test/test_doctest/test_doctest.py +++ b/Lib/test/test_doctest/test_doctest.py @@ -204,7 +204,7 @@ def test_Example(): r""" ... options={doctest.ELLIPSIS: True}) >>> (example.source, example.want, example.exc_msg, ... example.lineno, example.indent, example.options) - ('[].pop()\n', '', 'IndexError: pop from an empty list\n', 5, 4, {8: True}) + ('[].pop()\n', '', 'IndexError: pop from an empty list\n', 5, 4, {16: True}) The constructor normalizes the `source` string to end in a newline: @@ -1396,6 +1396,115 @@ def optionflags(): r""" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] +The IGNORE_LINEBREAK flag causes all sequences of newlines to be removed, +but retains the leading whitespaces as they cannot be distinguished from +real textual whitespaces: + + >>> def f(x): pass + >>> f.__doc__ = ''' + ... >>> "foobar" + ... 'foo + ... bar' + ... '''.strip() + + >>> # Without the flag: + >>> test = doctest.DocTestFinder().find(f)[0] + >>> doctest.DocTestRunner(verbose=False).run(test) + ... # doctest: +ELLIPSIS + ********************************************************************** + File ..., line ?, in f + Failed example: + "foobar" + Expected: + 'foo + bar' + Got: + 'foobar' + TestResults(failed=1, attempted=1) + + >>> # With the flag: + >>> test = doctest.DocTestFinder().find(f)[0] + >>> flags = doctest.IGNORE_LINEBREAK + >>> doctest.DocTestRunner(verbose=False, optionflags=flags).run(test) + TestResults(failed=0, attempted=1) + + ... ignore surrounding new lines + + >>> "foobar" # doctest: +IGNORE_LINEBREAK + ' + foo + bar' + >>> "foobar" # doctest: +IGNORE_LINEBREAK + 'foo + bar + ' + >>> "foobar" # doctest: +IGNORE_LINEBREAK + ' + foo + bar + ' + + ... non-quoted output: + + >>> import string + >>> print(string.ascii_letters) # doctest: +IGNORE_LINEBREAK + abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + + ... mixing flags: + + >>> import string + >>> print(string.ascii_letters) # doctest: +ELLIPSIS, +IGNORE_LINEBREAK + abc...xyz + ABC... + + ... mixing flags: + + >>> print(list("abc123")) # doctest: +IGNORE_LINEBREAK + ... # doctest: +ELLIPSIS + ... # doctest: +NORMALIZE_WHITESPACE + ['a', ..., 'c', + '1', ..., '3'] + + >>> prelude = r''' + ... >>> print(list("abc123")) # doctest: +IGNORE_LINEBREAK + ... ... # doctest: +ELLIPSIS + ... ... # doctest: +NORMALIZE_WHITESPACE + ... '''.strip() + + >>> def good(x): pass + >>> good.__doc__ = '\n'.join([prelude, r''' + ... ['a', ..., 'c', + ... '1', ..., '3'] + ... '''.lstrip()]).lstrip() + >>> test = doctest.DocTestFinder().find(good)[0] + >>> doctest.DocTestRunner(verbose=False).run(test) + TestResults(failed=0, attempted=1) + + >>> def fail(x): pass + >>> fail.__doc__ = '\n'.join([prelude, ''' + ... [ + ... 'a', ..., 'c', + ... '1', ..., '3' + ... ]\n'''.lstrip()]) + >>> test = doctest.DocTestFinder().find(fail)[0] + >>> doctest.DocTestRunner(verbose=False).run(test) + ... # doctest: +ELLIPSIS + ********************************************************************** + File ..., line ?, in fail + Failed example: + print(list("abc123")) # doctest: +IGNORE_LINEBREAK + # doctest: +ELLIPSIS + # doctest: +NORMALIZE_WHITESPACE + Expected: + [ + 'a', ..., 'c', + '1', ..., '3' + ] + Got: + ['a', 'b', 'c', '1', '2', '3'] + TestResults(failed=1, attempted=1) + The ELLIPSIS flag causes ellipsis marker ("...") in the expected output to match any substring in the actual output: diff --git a/Misc/NEWS.d/next/Library/2025-08-25-13-31-25.gh-issue-138135.yo5w-T.rst b/Misc/NEWS.d/next/Library/2025-08-25-13-31-25.gh-issue-138135.yo5w-T.rst new file mode 100644 index 00000000000000..18f41952de0a12 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-25-13-31-25.gh-issue-138135.yo5w-T.rst @@ -0,0 +1,3 @@ +:mod:`doctest`: Add :data:`~doctest.IGNORE_LINEBREAK` option to allow +breaking expected output strings without whitespaces into multiple lines. +Patch by Bénédikt Tran.