Skip to content

Commit 30e3249

Browse files
RaphaelFakhrifreider
authored andcommitted
Fix line numbers of code fences in docstrings
The docstring line offset assumed that the opening quotes are on their own line. Fences in docstrings with a summary on the opening line reported the line after the actual one, and fences in module docstrings reported the line before it. Docstrings with a string prefix (r""") were matched against their closing quotes, or not collected at all when the closing quotes share a line with other text.
1 parent 4fcd3da commit 30e3249

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

‎src/pytest_markdown_docs/plugin.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,41 @@ class FenceSyntax(Enum):
3636
superfences = "superfences"
3737

3838

39+
# Opening quotes of a docstring, with an optional string prefix (e.g. r""")
40+
DOCSTRING_OPENINGS = tuple(
41+
prefix + quotes for prefix in ("", "r", "R", "u", "U") for quotes in ('"""', "'''")
42+
)
43+
44+
3945
def get_docstring_start_line(obj) -> typing.Optional[int]:
46+
"""Get the line offset of the docstring text returned by `inspect.getdoc`
47+
48+
The offset is the line number of the first line of that text, minus one.
49+
"""
4050
# Get the source lines and the starting line number of the object
4151
try:
4252
source_lines, start_line = inspect.getsourcelines(obj)
4353
except OSError:
4454
return None
4555

56+
# getsourcelines returns 0 as the starting line number of a module
57+
start_line = max(start_line, 1)
58+
59+
# inspect.getdoc strips the leading blank lines of the docstring
60+
leading_blank_lines = 0
61+
raw_docstring = getattr(obj, "__doc__", None)
62+
if isinstance(raw_docstring, str):
63+
for raw_line in raw_docstring.split("\n"):
64+
if raw_line.strip():
65+
break
66+
leading_blank_lines += 1
67+
4668
# Find the line in the source code that starts with triple quotes (""" or ''')
4769
for idx, line in enumerate(source_lines):
4870
line = line.strip()
49-
if line.startswith(('"""', "'''")):
50-
return start_line + idx # Return the starting line number
71+
if line.startswith(DOCSTRING_OPENINGS):
72+
# idx is 0-based, so start_line + idx is the line of the opening quotes
73+
return start_line + idx - 1 + leading_blank_lines
5174

5275
return None # Docstring not found in source
5376

‎tests/plugin_test.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,71 @@ def test_error_origin_before_docstring_traceback(testdir, support_dir):
489489
)
490490

491491

492+
def test_docstring_line_numbers(testdir):
493+
testdir.makepyfile(
494+
docstring_lines='''
495+
def summary_on_first_line():
496+
"""Summary line.
497+
498+
```python
499+
raise Exception("foo")
500+
```
501+
"""
502+
503+
504+
def raw_docstring():
505+
r"""Matches \\d.
506+
507+
```python
508+
raise Exception("bar")
509+
```"""
510+
'''
511+
)
512+
result = testdir.runpytest("-v", "--markdown-docs")
513+
result.assert_outcomes(failed=2)
514+
result.stdout.re_match_lines(
515+
[
516+
r".*::raw_docstring\[CodeFence#1\]\[line:13\] FAILED.*",
517+
r".*::summary_on_first_line\[CodeFence#1\]\[line:4\] FAILED.*",
518+
],
519+
consecutive=True,
520+
)
521+
result.stdout.re_match_lines(
522+
[
523+
r'\s*File ".*/docstring_lines.py", line 14, in <module>',
524+
r'\s*raise Exception\("bar"\)',
525+
],
526+
consecutive=True,
527+
)
528+
result.stdout.re_match_lines(
529+
[
530+
r'\s*File ".*/docstring_lines.py", line 5, in <module>',
531+
r'\s*raise Exception\("foo"\)',
532+
],
533+
consecutive=True,
534+
)
535+
536+
537+
def test_module_docstring_line_numbers(testdir):
538+
testdir.makepyfile(
539+
module_docstring='''
540+
"""
541+
```python
542+
raise Exception("foo")
543+
```
544+
"""
545+
'''
546+
)
547+
result = testdir.runpytest("-v", "--markdown-docs")
548+
result.assert_outcomes(failed=1)
549+
result.stdout.re_match_lines(
550+
[
551+
r".*::module_docstring\[CodeFence#1\]\[line:2\] FAILED.*",
552+
r'\s*File ".*/module_docstring.py", line 3, in <module>',
553+
]
554+
)
555+
556+
492557
def test_custom_runner(testdir):
493558
testdir.makeconftest(
494559
"""

0 commit comments

Comments
 (0)