Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/pytest_markdown_docs/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,41 @@ class FenceSyntax(Enum):
superfences = "superfences"


# Opening quotes of a docstring, with an optional string prefix (e.g. r""")
DOCSTRING_OPENINGS = tuple(
prefix + quotes for prefix in ("", "r", "R", "u", "U") for quotes in ('"""', "'''")
)


def get_docstring_start_line(obj) -> typing.Optional[int]:
"""Get the line offset of the docstring text returned by `inspect.getdoc`

The offset is the line number of the first line of that text, minus one.
"""
# Get the source lines and the starting line number of the object
try:
source_lines, start_line = inspect.getsourcelines(obj)
except OSError:
return None

# getsourcelines returns 0 as the starting line number of a module
start_line = max(start_line, 1)

# inspect.getdoc strips the leading blank lines of the docstring
leading_blank_lines = 0
raw_docstring = getattr(obj, "__doc__", None)
if isinstance(raw_docstring, str):
for raw_line in raw_docstring.split("\n"):
if raw_line.strip():
break
leading_blank_lines += 1

# Find the line in the source code that starts with triple quotes (""" or ''')
for idx, line in enumerate(source_lines):
line = line.strip()
if line.startswith(('"""', "'''")):
return start_line + idx # Return the starting line number
if line.startswith(DOCSTRING_OPENINGS):
# idx is 0-based, so start_line + idx is the line of the opening quotes
return start_line + idx - 1 + leading_blank_lines

return None # Docstring not found in source

Expand Down
65 changes: 65 additions & 0 deletions tests/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,71 @@ def test_error_origin_before_docstring_traceback(testdir, support_dir):
)


def test_docstring_line_numbers(testdir):
testdir.makepyfile(
docstring_lines='''
def summary_on_first_line():
"""Summary line.

```python
raise Exception("foo")
```
"""


def raw_docstring():
r"""Matches \\d.

```python
raise Exception("bar")
```"""
'''
)
result = testdir.runpytest("-v", "--markdown-docs")
result.assert_outcomes(failed=2)
result.stdout.re_match_lines(
[
r".*::raw_docstring\[CodeFence#1\]\[line:13\] FAILED.*",
r".*::summary_on_first_line\[CodeFence#1\]\[line:4\] FAILED.*",
],
consecutive=True,
)
result.stdout.re_match_lines(
[
r'\s*File ".*/docstring_lines.py", line 14, in <module>',
r'\s*raise Exception\("bar"\)',
],
consecutive=True,
)
result.stdout.re_match_lines(
[
r'\s*File ".*/docstring_lines.py", line 5, in <module>',
r'\s*raise Exception\("foo"\)',
],
consecutive=True,
)


def test_module_docstring_line_numbers(testdir):
testdir.makepyfile(
module_docstring='''
"""
```python
raise Exception("foo")
```
"""
'''
)
result = testdir.runpytest("-v", "--markdown-docs")
result.assert_outcomes(failed=1)
result.stdout.re_match_lines(
[
r".*::module_docstring\[CodeFence#1\]\[line:2\] FAILED.*",
r'\s*File ".*/module_docstring.py", line 3, in <module>',
]
)


def test_custom_runner(testdir):
testdir.makeconftest(
"""
Expand Down
Loading