Skip to content

Commit 100e316

Browse files
CuriousLearnerJurjen N.E. Bosbitdancer
authored
gh-69113: Fix doctest to report line numbers for __test__ strings (#141624)
Enhanced the _find_lineno method in doctest to correctly identify and report line numbers for doctests defined in __test__ dictionaries when formatted as triple-quoted strings. Finds a non-blank line in the test string and matches it in the source file, verifying subsequent lines also match to handle duplicate lines. Previously, doctest would report "line None" for __test__ dictionary strings, making it difficult to debug failing tests. Co-authored-by: Jurjen N.E. Bos <[email protected]> Co-authored-by: R. David Murray <[email protected]>
1 parent c91c373 commit 100e316

File tree

3 files changed

+145
-4
lines changed

3 files changed

+145
-4
lines changed

Lib/doctest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,32 @@ def _find_lineno(self, obj, source_lines):
11671167
if pat.match(source_lines[lineno]):
11681168
return lineno
11691169

1170+
# Handle __test__ string doctests formatted as triple-quoted
1171+
# strings. Find a non-blank line in the test string and match it
1172+
# in the source, verifying subsequent lines also match to handle
1173+
# duplicate lines.
1174+
if isinstance(obj, str) and source_lines is not None:
1175+
obj_lines = obj.splitlines(keepends=True)
1176+
# Skip the first line (may be on same line as opening quotes)
1177+
# and any blank lines to find a meaningful line to match.
1178+
start_index = 1
1179+
while (start_index < len(obj_lines)
1180+
and not obj_lines[start_index].strip()):
1181+
start_index += 1
1182+
if start_index < len(obj_lines):
1183+
target_line = obj_lines[start_index]
1184+
for lineno, source_line in enumerate(source_lines):
1185+
if source_line == target_line:
1186+
# Verify subsequent lines also match
1187+
for i in range(start_index + 1, len(obj_lines) - 1):
1188+
source_idx = lineno + i - start_index
1189+
if source_idx >= len(source_lines):
1190+
break
1191+
if obj_lines[i] != source_lines[source_idx]:
1192+
break
1193+
else:
1194+
return lineno - start_index
1195+
11701196
# We couldn't find the line number.
11711197
return None
11721198

Lib/test/test_doctest/test_doctest.py

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,118 @@ def test_empty_namespace_package(self):
833833
self.assertEqual(len(include_empty_finder.find(mod)), 1)
834834
self.assertEqual(len(exclude_empty_finder.find(mod)), 0)
835835

836+
def test_lineno_of_test_dict_strings(self):
837+
"""Test line numbers are found for __test__ dict strings."""
838+
module_content = '''\
839+
"""Module docstring."""
840+
841+
def dummy_function():
842+
"""Dummy function docstring."""
843+
pass
844+
845+
__test__ = {
846+
'test_string': """
847+
This is a test string.
848+
>>> 1 + 1
849+
2
850+
""",
851+
}
852+
'''
853+
with tempfile.TemporaryDirectory() as tmpdir:
854+
module_path = os.path.join(tmpdir, 'test_module_lineno.py')
855+
with open(module_path, 'w') as f:
856+
f.write(module_content)
857+
858+
sys.path.insert(0, tmpdir)
859+
try:
860+
import test_module_lineno
861+
finder = doctest.DocTestFinder()
862+
tests = finder.find(test_module_lineno)
863+
864+
test_dict_test = None
865+
for test in tests:
866+
if '__test__' in test.name:
867+
test_dict_test = test
868+
break
869+
870+
self.assertIsNotNone(
871+
test_dict_test,
872+
"__test__ dict test not found"
873+
)
874+
# gh-69113: line number should not be None for __test__ strings
875+
self.assertIsNotNone(
876+
test_dict_test.lineno,
877+
"Line number should not be None for __test__ dict strings"
878+
)
879+
self.assertGreater(
880+
test_dict_test.lineno,
881+
0,
882+
"Line number should be positive"
883+
)
884+
finally:
885+
if 'test_module_lineno' in sys.modules:
886+
del sys.modules['test_module_lineno']
887+
sys.path.pop(0)
888+
889+
def test_lineno_multiline_matching(self):
890+
"""Test multi-line matching when no unique line exists."""
891+
# gh-69113: test that line numbers are found even when lines
892+
# appear multiple times (e.g., ">>> x = 1" in both test entries)
893+
module_content = '''\
894+
"""Module docstring."""
895+
896+
__test__ = {
897+
'test_one': """
898+
>>> x = 1
899+
>>> x
900+
1
901+
""",
902+
'test_two': """
903+
>>> x = 1
904+
>>> x
905+
2
906+
""",
907+
}
908+
'''
909+
with tempfile.TemporaryDirectory() as tmpdir:
910+
module_path = os.path.join(tmpdir, 'test_module_multiline.py')
911+
with open(module_path, 'w') as f:
912+
f.write(module_content)
913+
914+
sys.path.insert(0, tmpdir)
915+
try:
916+
import test_module_multiline
917+
finder = doctest.DocTestFinder()
918+
tests = finder.find(test_module_multiline)
919+
920+
test_one = None
921+
test_two = None
922+
for test in tests:
923+
if 'test_one' in test.name:
924+
test_one = test
925+
elif 'test_two' in test.name:
926+
test_two = test
927+
928+
self.assertIsNotNone(test_one, "test_one not found")
929+
self.assertIsNotNone(test_two, "test_two not found")
930+
self.assertIsNotNone(
931+
test_one.lineno,
932+
"Line number should not be None for test_one"
933+
)
934+
self.assertIsNotNone(
935+
test_two.lineno,
936+
"Line number should not be None for test_two"
937+
)
938+
self.assertNotEqual(
939+
test_one.lineno,
940+
test_two.lineno,
941+
"test_one and test_two should have different line numbers"
942+
)
943+
finally:
944+
if 'test_module_multiline' in sys.modules:
945+
del sys.modules['test_module_multiline']
946+
sys.path.pop(0)
947+
836948
def test_DocTestParser(): r"""
837949
Unit tests for the `DocTestParser` class.
838950
@@ -2434,7 +2546,8 @@ def test_DocTestSuite_errors():
24342546
<BLANKLINE>
24352547
>>> print(result.failures[1][1]) # doctest: +ELLIPSIS
24362548
Traceback (most recent call last):
2437-
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
2549+
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
2550+
>...>> 2 + 2
24382551
AssertionError: Failed example:
24392552
2 + 2
24402553
Expected:
@@ -2464,7 +2577,8 @@ def test_DocTestSuite_errors():
24642577
<BLANKLINE>
24652578
>>> print(result.errors[1][1]) # doctest: +ELLIPSIS
24662579
Traceback (most recent call last):
2467-
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
2580+
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
2581+
>...>> 1/0
24682582
File "<doctest test.test_doctest.sample_doctest_errors.__test__.bad[1]>", line 1, in <module>
24692583
1/0
24702584
~^~
@@ -3256,15 +3370,15 @@ def test_testmod_errors(): r"""
32563370
~^~
32573371
ZeroDivisionError: division by zero
32583372
**********************************************************************
3259-
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
3373+
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
32603374
Failed example:
32613375
2 + 2
32623376
Expected:
32633377
5
32643378
Got:
32653379
4
32663380
**********************************************************************
3267-
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
3381+
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
32683382
Failed example:
32693383
1/0
32703384
Exception raised:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :mod:`doctest` to correctly report line numbers for doctests in ``__test__`` dictionary when formatted as triple-quoted strings by finding unique lines in the string and matching them in the source file.

0 commit comments

Comments
 (0)