Skip to content

Commit 55e987b

Browse files
CuriousLearnerJurjen N.E. Bosbitdancer
committed
gh-69113: Fix doctest to report line numbers for __test__ strings
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 <jneb@users.sourceforge.net> Co-Authored-By: R. David Murray <rdmurray@bitdance.com>
1 parent 229ed3d commit 55e987b

File tree

3 files changed

+148
-4
lines changed

3 files changed

+148
-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 strings.
1171+
# Find a non-blank line in the test string and match it in the source,
1172+
# verifying subsequent lines also match to handle duplicate lines.
1173+
if isinstance(obj, str) and source_lines is not None:
1174+
obj_lines = obj.splitlines(keepends=True)
1175+
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) and not obj_lines[start_index].strip():
1180+
start_index += 1
1181+
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: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,121 @@ 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 that line numbers are correctly found for __test__ dict strings."""
838+
# Create a temporary module with a __test__ dict containing triple-quoted strings
839+
module_content = '''\
840+
"""Module docstring."""
841+
842+
def dummy_function():
843+
"""Dummy function docstring."""
844+
pass
845+
846+
__test__ = {
847+
'test_string': """
848+
This is a test string.
849+
>>> 1 + 1
850+
2
851+
""",
852+
}
853+
'''
854+
with tempfile.TemporaryDirectory() as tmpdir:
855+
module_path = os.path.join(tmpdir, 'test_module_lineno.py')
856+
with open(module_path, 'w') as f:
857+
f.write(module_content)
858+
859+
sys.path.insert(0, tmpdir)
860+
try:
861+
# Import the module
862+
import test_module_lineno
863+
864+
# Use DocTestFinder to find tests
865+
finder = doctest.DocTestFinder()
866+
tests = finder.find(test_module_lineno)
867+
868+
# Find the test from __test__ dict
869+
test_dict_test = None
870+
for test in tests:
871+
if '__test__' in test.name:
872+
test_dict_test = test
873+
break
874+
875+
# Assert that we found the test and it has a valid line number
876+
self.assertIsNotNone(test_dict_test, "__test__ dict test not found")
877+
# The line number should not be None - this is what the bug fix addresses
878+
self.assertIsNotNone(test_dict_test.lineno,
879+
"Line number should not be None for __test__ dict strings")
880+
# The line number should be around line 9 (where the triple-quoted string starts)
881+
# Allowing some tolerance for exact line number
882+
self.assertGreater(test_dict_test.lineno, 0,
883+
"Line number should be positive")
884+
finally:
885+
# Clean up
886+
if 'test_module_lineno' in sys.modules:
887+
del sys.modules['test_module_lineno']
888+
sys.path.pop(0)
889+
890+
def test_lineno_fallback_multiline_matching(self):
891+
"""Test fallback to multi-line matching when no unique line exists."""
892+
# Create a module where the same lines appear multiple times,
893+
# forcing the fallback to multi-line matching
894+
module_content = '''\
895+
"""Module docstring."""
896+
897+
# These lines appear in both __test__ entries
898+
# >>> x = 1
899+
# >>> x
900+
# 1
901+
902+
__test__ = {
903+
'test_one': """
904+
>>> x = 1
905+
>>> x
906+
1
907+
""",
908+
'test_two': """
909+
>>> x = 1
910+
>>> x
911+
2
912+
""",
913+
}
914+
'''
915+
with tempfile.TemporaryDirectory() as tmpdir:
916+
module_path = os.path.join(tmpdir, 'test_module_fallback.py')
917+
with open(module_path, 'w') as f:
918+
f.write(module_content)
919+
920+
sys.path.insert(0, tmpdir)
921+
try:
922+
import test_module_fallback
923+
924+
finder = doctest.DocTestFinder()
925+
tests = finder.find(test_module_fallback)
926+
927+
# Find both __test__ entries
928+
test_one = None
929+
test_two = None
930+
for test in tests:
931+
if 'test_one' in test.name:
932+
test_one = test
933+
elif 'test_two' in test.name:
934+
test_two = test
935+
936+
# Both should have valid line numbers found via fallback
937+
self.assertIsNotNone(test_one, "test_one not found")
938+
self.assertIsNotNone(test_two, "test_two not found")
939+
self.assertIsNotNone(test_one.lineno,
940+
"Line number should not be None for test_one")
941+
self.assertIsNotNone(test_two.lineno,
942+
"Line number should not be None for test_two")
943+
# They should have different line numbers
944+
self.assertNotEqual(test_one.lineno, test_two.lineno,
945+
"test_one and test_two should have different line numbers")
946+
finally:
947+
if 'test_module_fallback' in sys.modules:
948+
del sys.modules['test_module_fallback']
949+
sys.path.pop(0)
950+
836951
def test_DocTestParser(): r"""
837952
Unit tests for the `DocTestParser` class.
838953
@@ -2434,7 +2549,8 @@ def test_DocTestSuite_errors():
24342549
<BLANKLINE>
24352550
>>> print(result.failures[1][1]) # doctest: +ELLIPSIS
24362551
Traceback (most recent call last):
2437-
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
2552+
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
2553+
>...>> 2 + 2
24382554
AssertionError: Failed example:
24392555
2 + 2
24402556
Expected:
@@ -2464,7 +2580,8 @@ def test_DocTestSuite_errors():
24642580
<BLANKLINE>
24652581
>>> print(result.errors[1][1]) # doctest: +ELLIPSIS
24662582
Traceback (most recent call last):
2467-
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
2583+
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
2584+
>...>> 1/0
24682585
File "<doctest test.test_doctest.sample_doctest_errors.__test__.bad[1]>", line 1, in <module>
24692586
1/0
24702587
~^~
@@ -3256,15 +3373,15 @@ def test_testmod_errors(): r"""
32563373
~^~
32573374
ZeroDivisionError: division by zero
32583375
**********************************************************************
3259-
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
3376+
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
32603377
Failed example:
32613378
2 + 2
32623379
Expected:
32633380
5
32643381
Got:
32653382
4
32663383
**********************************************************************
3267-
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
3384+
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
32683385
Failed example:
32693386
1/0
32703387
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)