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]):
|
if pat.match(source_lines[lineno]):
|
||||||
return 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.
|
# We couldn't find the line number.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -833,6 +833,118 @@ def test_empty_namespace_package(self):
|
||||||
self.assertEqual(len(include_empty_finder.find(mod)), 1)
|
self.assertEqual(len(include_empty_finder.find(mod)), 1)
|
||||||
self.assertEqual(len(exclude_empty_finder.find(mod)), 0)
|
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"""
|
def test_DocTestParser(): r"""
|
||||||
Unit tests for the `DocTestParser` class.
|
Unit tests for the `DocTestParser` class.
|
||||||
|
|
||||||
|
|
@ -2434,7 +2546,8 @@ def test_DocTestSuite_errors():
|
||||||
<BLANKLINE>
|
<BLANKLINE>
|
||||||
>>> print(result.failures[1][1]) # doctest: +ELLIPSIS
|
>>> print(result.failures[1][1]) # doctest: +ELLIPSIS
|
||||||
Traceback (most recent call last):
|
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:
|
AssertionError: Failed example:
|
||||||
2 + 2
|
2 + 2
|
||||||
Expected:
|
Expected:
|
||||||
|
|
@ -2464,7 +2577,8 @@ def test_DocTestSuite_errors():
|
||||||
<BLANKLINE>
|
<BLANKLINE>
|
||||||
>>> print(result.errors[1][1]) # doctest: +ELLIPSIS
|
>>> print(result.errors[1][1]) # doctest: +ELLIPSIS
|
||||||
Traceback (most recent call last):
|
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>
|
File "<doctest test.test_doctest.sample_doctest_errors.__test__.bad[1]>", line 1, in <module>
|
||||||
1/0
|
1/0
|
||||||
~^~
|
~^~
|
||||||
|
|
@ -3256,7 +3370,7 @@ def test_testmod_errors(): r"""
|
||||||
~^~
|
~^~
|
||||||
ZeroDivisionError: division by zero
|
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:
|
Failed example:
|
||||||
2 + 2
|
2 + 2
|
||||||
Expected:
|
Expected:
|
||||||
|
|
@ -3264,7 +3378,7 @@ def test_testmod_errors(): r"""
|
||||||
Got:
|
Got:
|
||||||
4
|
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:
|
Failed example:
|
||||||
1/0
|
1/0
|
||||||
Exception raised:
|
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