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
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.
<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:
<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
~^~
~^~
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:
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: