Skip to content

Commit

Permalink
feat: Support admonitions in Numpydoc docstrings
Browse files Browse the repository at this point in the history
Previously, sections titled "See also" and others were not
recognized as sections, and therefore just parsed as regular text.
Now, such unknown sections will be parsed as admonitions.

In a previous breaking change, support for prose between sections
was removed: this change compensantes the breakage by allowing
users to use admonitions such as Note, Important, etc., to re-add
prose between sections thanks to admonitions.

This also further reduces the gap between Numpydoc and Google-style.

Issue #214: #214
PR #219: #219
Co-authored-by: Timothée Mazzucotelli <[email protected]>
  • Loading branch information
machow authored Jan 15, 2024
1 parent 8c57354 commit 1e311a4
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 12 deletions.
64 changes: 52 additions & 12 deletions src/griffe/docstrings/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
DocstringReceive,
DocstringReturn,
DocstringSection,
DocstringSectionAdmonition,
DocstringSectionAttributes,
DocstringSectionClasses,
DocstringSectionDeprecated,
Expand Down Expand Up @@ -720,6 +721,19 @@ def _read_examples_section(
return None, new_offset


def _append_section(sections: list, current: list[str], admonition_title: str) -> None:
if admonition_title:
sections.append(
DocstringSectionAdmonition(
kind=admonition_title.lower().replace(" ", "-"),
text="\n".join(current).rstrip("\n"),
title=admonition_title,
),
)
elif current and any(current):
sections.append(DocstringSectionText("\n".join(current).rstrip("\n")))


_section_reader = {
DocstringSectionKind.parameters: _read_parameters_section,
DocstringSectionKind.other_parameters: _read_other_parameters_section,
Expand Down Expand Up @@ -762,6 +776,7 @@ def parse(
"""
sections: list[DocstringSection] = []
current_section = []
admonition_title = ""

in_code_block = False
lines = docstring.lines
Expand All @@ -787,32 +802,57 @@ def parse(
while offset < len(lines):
line_lower = lines[offset].lower()

# Code blocks can contain dash lines that we must not interpret.
if in_code_block:
# End of code block.
if line_lower.lstrip(" ").startswith("```"):
in_code_block = False
# Lines in code block must not be interpreted in any way.
current_section.append(lines[offset])

elif line_lower in _section_kind and _is_dash_line(lines[offset + 1]):
if current_section:
if any(current_section):
sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
current_section = []
reader = _section_reader[_section_kind[line_lower]]
section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator]
if section:
sections.append(section)

# Start of code block.
elif line_lower.lstrip(" ").startswith("```"):
in_code_block = True
current_section.append(lines[offset])

# Dash lines after empty lines lose their meaning.
elif _is_empty_line(lines[offset]):
current_section.append("")

# End of the docstring, wrap up.
elif offset == len(lines) - 1:
current_section.append(lines[offset])
_append_section(sections, current_section, admonition_title)
admonition_title = ""
current_section = []

# Dash line after regular, non-empty line.
elif _is_dash_line(lines[offset + 1]):
# Finish reading current section.
_append_section(sections, current_section, admonition_title)
current_section = []

# Start parsing new (known) section.
if line_lower in _section_kind:
admonition_title = ""
reader = _section_reader[_section_kind[line_lower]]
section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator]
if section:
sections.append(section)

# Start parsing admonition.
else:
admonition_title = lines[offset]
offset += 1 # skip next dash line

# Regular line.
else:
current_section.append(lines[offset])

offset += 1

if current_section:
sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n")))
# Finish current section.
_append_section(sections, current_section, admonition_title)

return sections

Expand Down
72 changes: 72 additions & 0 deletions tests/test_docstrings/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,78 @@ def test_doubly_indented_lines_in_section_items(parse_numpy: ParserType) -> None
assert lines[-1].startswith(4 * " " + "- ")


# =============================================================================================
# Admonitions
def test_admonition_see_also(parse_numpy: ParserType) -> None:
"""Test a "See Also" admonition.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Summary text.
See Also
--------
some_function
more text
"""

sections, _ = parse_numpy(docstring)
assert len(sections) == 2
assert sections[0].value == "Summary text."
assert sections[1].title == "See Also"
assert sections[1].value.description == "some_function\n\nmore text"


def test_admonition_empty(parse_numpy: ParserType) -> None:
"""Test an empty "See Also" admonition.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Summary text.
See Also
--------
"""

sections, _ = parse_numpy(docstring)
assert len(sections) == 2
assert sections[0].value == "Summary text."
assert sections[1].title == "See Also"
assert sections[1].value.description == ""


def test_isolated_dash_lines_do_not_create_sections(parse_numpy: ParserType) -> None:
"""An isolated dash-line (`---`) should not be parsed as a section.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Summary text.
---
Text.
Note
----
Note contents.
---
Text.
"""

sections, _ = parse_numpy(docstring)
assert len(sections) == 2
assert sections[0].value == "Summary text.\n\n---\nText."
assert sections[1].title == "Note"
assert sections[1].value.description == "Note contents.\n\n---\nText."


# =============================================================================================
# Annotations
def test_prefer_docstring_type_over_annotation(parse_numpy: ParserType) -> None:
Expand Down

0 comments on commit 1e311a4

Please sign in to comment.