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 <jneb@users.sourceforge.net>
Co-authored-by: R. David Murray <rdmurray@bitdance.com>
This commit is contained in:
Sanyam Khurana 2025-12-06 15:47:08 -05:00 committed by GitHub
parent c91c373ef6
commit 100e316e53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 145 additions and 4 deletions

View file

@ -1167,6 +1167,32 @@ def _find_lineno(self, obj, source_lines):
if pat.match(source_lines[lineno]):
return lineno
# Handle __test__ string doctests formatted as triple-quoted
# strings. Find a non-blank line in the test string and match it
# in the source, verifying subsequent lines also match to handle
# duplicate lines.
if isinstance(obj, str) and source_lines is not None:
obj_lines = obj.splitlines(keepends=True)
# Skip the first line (may be on same line as opening quotes)
# and any blank lines to find a meaningful line to match.
start_index = 1
while (start_index < len(obj_lines)
and not obj_lines[start_index].strip()):
start_index += 1
if start_index < len(obj_lines):
target_line = obj_lines[start_index]
for lineno, source_line in enumerate(source_lines):
if source_line == target_line:
# Verify subsequent lines also match
for i in range(start_index + 1, len(obj_lines) - 1):
source_idx = lineno + i - start_index
if source_idx >= len(source_lines):
break
if obj_lines[i] != source_lines[source_idx]:
break
else:
return lineno - start_index
# We couldn't find the line number.
return None

View file

@ -833,6 +833,118 @@ def test_empty_namespace_package(self):
self.assertEqual(len(include_empty_finder.find(mod)), 1)
self.assertEqual(len(exclude_empty_finder.find(mod)), 0)
def test_lineno_of_test_dict_strings(self):
"""Test line numbers are found for __test__ dict strings."""
module_content = '''\
"""Module docstring."""
def dummy_function():
"""Dummy function docstring."""
pass
__test__ = {
'test_string': """
This is a test string.
>>> 1 + 1
2
""",
}
'''
with tempfile.TemporaryDirectory() as tmpdir:
module_path = os.path.join(tmpdir, 'test_module_lineno.py')
with open(module_path, 'w') as f:
f.write(module_content)
sys.path.insert(0, tmpdir)
try:
import test_module_lineno
finder = doctest.DocTestFinder()
tests = finder.find(test_module_lineno)
test_dict_test = None
for test in tests:
if '__test__' in test.name:
test_dict_test = test
break
self.assertIsNotNone(
test_dict_test,
"__test__ dict test not found"
)
# gh-69113: line number should not be None for __test__ strings
self.assertIsNotNone(
test_dict_test.lineno,
"Line number should not be None for __test__ dict strings"
)
self.assertGreater(
test_dict_test.lineno,
0,
"Line number should be positive"
)
finally:
if 'test_module_lineno' in sys.modules:
del sys.modules['test_module_lineno']
sys.path.pop(0)
def test_lineno_multiline_matching(self):
"""Test multi-line matching when no unique line exists."""
# gh-69113: test that line numbers are found even when lines
# appear multiple times (e.g., ">>> x = 1" in both test entries)
module_content = '''\
"""Module docstring."""
__test__ = {
'test_one': """
>>> x = 1
>>> x
1
""",
'test_two': """
>>> x = 1
>>> x
2
""",
}
'''
with tempfile.TemporaryDirectory() as tmpdir:
module_path = os.path.join(tmpdir, 'test_module_multiline.py')
with open(module_path, 'w') as f:
f.write(module_content)
sys.path.insert(0, tmpdir)
try:
import test_module_multiline
finder = doctest.DocTestFinder()
tests = finder.find(test_module_multiline)
test_one = None
test_two = None
for test in tests:
if 'test_one' in test.name:
test_one = test
elif 'test_two' in test.name:
test_two = test
self.assertIsNotNone(test_one, "test_one not found")
self.assertIsNotNone(test_two, "test_two not found")
self.assertIsNotNone(
test_one.lineno,
"Line number should not be None for test_one"
)
self.assertIsNotNone(
test_two.lineno,
"Line number should not be None for test_two"
)
self.assertNotEqual(
test_one.lineno,
test_two.lineno,
"test_one and test_two should have different line numbers"
)
finally:
if 'test_module_multiline' in sys.modules:
del sys.modules['test_module_multiline']
sys.path.pop(0)
def test_DocTestParser(): r"""
Unit tests for the `DocTestParser` class.
@ -2434,7 +2546,8 @@ def test_DocTestSuite_errors():
<BLANKLINE>
>>> print(result.failures[1][1]) # doctest: +ELLIPSIS
Traceback (most recent call last):
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
>...>> 2 + 2
AssertionError: Failed example:
2 + 2
Expected:
@ -2464,7 +2577,8 @@ def test_DocTestSuite_errors():
<BLANKLINE>
>>> print(result.errors[1][1]) # doctest: +ELLIPSIS
Traceback (most recent call last):
File "...sample_doctest_errors.py", line None, in test.test_doctest.sample_doctest_errors.__test__.bad
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
>...>> 1/0
File "<doctest test.test_doctest.sample_doctest_errors.__test__.bad[1]>", line 1, in <module>
1/0
~^~
@ -3256,7 +3370,7 @@ def test_testmod_errors(): r"""
~^~
ZeroDivisionError: division by zero
**********************************************************************
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
File "...sample_doctest_errors.py", line 37, in test.test_doctest.sample_doctest_errors.__test__.bad
Failed example:
2 + 2
Expected:
@ -3264,7 +3378,7 @@ def test_testmod_errors(): r"""
Got:
4
**********************************************************************
File "...sample_doctest_errors.py", line ?, in test.test_doctest.sample_doctest_errors.__test__.bad
File "...sample_doctest_errors.py", line 39, in test.test_doctest.sample_doctest_errors.__test__.bad
Failed example:
1/0
Exception raised:

View file

@ -0,0 +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.