mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
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:
parent
c91c373ef6
commit
100e316e53
3 changed files with 145 additions and 4 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue