Skip to content

Commit

Permalink
fix: Consider space-only lines to be empty, never break Numpydoc sect…
Browse files Browse the repository at this point in the history
…ions on blank lines

Breaking change: This change removes the docstring option
`allow_section_blank_line` for the Numpydoc parser,
and changes the parsing logic so that blank lines are now *always* allowed,
in any number, between sections. Sections are now only delimited by
section headers themselves. The result of these changes is that:
**prose is not allowed in between Numpydoc sections anymore**,
making the parser compliant with the Numpydoc style guide and
its maintainers recommendations.

PR #220: #220
Related to PR #219: #219
Numpydoc discussion: numpy/numpydoc#463
  • Loading branch information
pawamoy committed Jan 14, 2024
1 parent 206d338 commit 8c57354
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 46 deletions.
4 changes: 0 additions & 4 deletions docs/docstrings.md
Original file line number Diff line number Diff line change
Expand Up @@ -832,10 +832,6 @@ The parser accepts a few options:
These flags are used to alter the behavior of [doctest][] when testing docstrings,
and should not be visible in your docs. Default: true.
- `warn_unknown_params`: Warn about parameters documented in docstrings that do not appear in the signature. Default: true.
- `allow_section_blank_line`: Allow blank lines in sections' content.
When false, a blank line finishes the current section.
When true, single blank lines are kept as part of the section.
You can terminate sections with double blank lines. Default: false.

#### Attributes

Expand Down
10 changes: 5 additions & 5 deletions src/griffe/docstrings/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ def _read_block_items(docstring: Docstring, *, offset: int, **options: Any) -> I
while new_offset < len(lines):
line = lines[new_offset]

if line.startswith(indent * 2 * " "):
if _is_empty_line(line):
# empty line: preserve it in the current item
current_item[1].append("")

elif line.startswith(indent * 2 * " "):
# continuation line
current_item[1].append(line[indent * 2 :])

Expand All @@ -125,10 +129,6 @@ def _read_block_items(docstring: Docstring, *, offset: int, **options: Any) -> I
f"should be {indent} * 2 = {indent*2} spaces, not {cont_indent}",
)

elif _is_empty_line(line):
# empty line: preserve it in the current item
current_item[1].append("")

elif line.startswith(indent * " "):
# indent equal to initial one: new item
items.append(current_item)
Expand Down
37 changes: 8 additions & 29 deletions src/griffe/docstrings/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ def _read_block_items(
docstring: Docstring,
*,
offset: int,
allow_section_blank_line: bool,
**options: Any, # noqa: ARG001
) -> tuple[list[list[str]], int]:
lines = docstring.lines
Expand All @@ -109,8 +108,6 @@ def _read_block_items(
while _is_empty_line(lines[new_offset]):
new_offset += 1

previous_was_empty = False

# start processing first item
current_item = [lines[new_offset]]
new_offset += 1
Expand All @@ -119,10 +116,13 @@ def _read_block_items(
while new_offset < len(lines):
line = lines[new_offset]

if line.startswith(4 * " "):
if _is_empty_line(line):
# empty line: preserve it in the current item
current_item.append("")

elif line.startswith(4 * " "):
# continuation line
current_item.append(line[4:])
previous_was_empty = False

elif line.startswith(" "):
# indent between initial and continuation: append but warn
Expand All @@ -134,30 +134,14 @@ def _read_block_items(
f"Confusing indentation for continuation line {new_offset+1} in docstring, "
f"should be 4 spaces, not {cont_indent}",
)
previous_was_empty = False

elif _is_empty_line(line):
# two line breaks indicate the start of a new section
if previous_was_empty:
break

# empty line: preserve it in the current item
current_item.append("")
previous_was_empty = True

else:
# preserve original behavior, that a single line break between block
# items triggers a new section
if not allow_section_blank_line and previous_was_empty:
break

elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]):
# detect the start of a new section
if new_offset + 1 < len(lines) and lines[new_offset + 1].startswith("---"):
break
break

else:
items.append(current_item)
current_item = [line]
previous_was_empty = False

new_offset += 1

Expand Down Expand Up @@ -758,7 +742,6 @@ def parse(
*,
ignore_init_summary: bool = False,
trim_doctest_flags: bool = True,
allow_section_blank_line: bool = False,
warn_unknown_params: bool = True,
**options: Any,
) -> list[DocstringSection]:
Expand All @@ -771,9 +754,6 @@ def parse(
docstring: The docstring to parse.
ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings.
trim_doctest_flags: Whether to remove doctest flags from Python example blocks.
allow_section_blank_line: Whether to continue a section if there's an empty line
between items in a formatted block, like Parameters or Returns.
If True, you can still create a new section using two empty lines.
warn_unknown_params: Warn about documented parameters not appearing in the signature.
**options: Additional parsing options.
Expand All @@ -789,7 +769,6 @@ def parse(
options = {
"trim_doctest_flags": trim_doctest_flags,
"ignore_init_summary": ignore_init_summary,
"allow_section_blank_line": allow_section_blank_line,
"warn_unknown_params": warn_unknown_params,
**options,
}
Expand Down
48 changes: 41 additions & 7 deletions tests/test_docstrings/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,8 @@ def test_empty_indented_lines_in_section_with_items(parse_numpy: ParserType) ->
"""
docstring = "Returns\n-------\nonly_item : type\n Description.\n \n \n\nSomething."
sections, _ = parse_numpy(docstring)
assert len(sections) == 2
assert len(sections[0].value) == 1

# allow_section_blank_line requires at least 2 newlines to create a new section
sections2, _ = parse_numpy(docstring, allow_section_blank_line=True)
assert len(sections2) == 1
assert len(sections2[0].value) == 2
assert len(sections) == 1
assert len(sections[0].value) == 2


def test_doubly_indented_lines_in_section_items(parse_numpy: ParserType) -> None:
Expand Down Expand Up @@ -711,6 +706,23 @@ def test_examples_section_as_last(parse_numpy: ParserType) -> None:
assert sections[1].kind is DocstringSectionKind.examples


def test_blank_lines_in_section(parse_numpy: ParserType) -> None:
"""Support blank lines in the middle of sections.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Examples
--------
Line 1.
Line 2.
"""
sections, _ = parse_numpy(docstring)
assert len(sections) == 1


# =============================================================================================
# Attributes sections
def test_retrieve_attributes_annotation_from_parent(parse_numpy: ParserType) -> None:
Expand Down Expand Up @@ -863,6 +875,28 @@ def test_detect_optional_flag(parse_numpy: ParserType) -> None:
assert sections[0].value[2].default == "b''"


@pytest.mark.parametrize("newlines", [1, 2, 3])
def test_blank_lines_in_item_descriptions(parse_numpy: ParserType, newlines: int) -> None:
"""Support blank lines in the middle of item descriptions.
Parameters:
parse_numpy: Fixture parser.
newlines: Number of new lines between item summary and its body.
"""
nl = "\n"
nlindent = "\n" + " " * 12
docstring = f"""
Parameters
----------
a : str
Summary.{nlindent * newlines}Body.
"""
sections, _ = parse_numpy(docstring)
assert len(sections) == 1
assert sections[0].value[0].annotation == "str"
assert sections[0].value[0].description == f"Summary.{nl * newlines}Body."


# =============================================================================================
# Yields sections
@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from __future__ import annotations

import sys
from pathlib import Path

import pytest

from griffe.agents.inspector import inspect
import sys
from griffe.tests import temporary_inspected_module, temporary_pypackage


Expand Down

0 comments on commit 8c57354

Please sign in to comment.