Skip to content

Commit

Permalink
feat: Allow deselecting multiple or named items in Yields and Receives
Browse files Browse the repository at this point in the history
Issue-263: #263
  • Loading branch information
the-13th-letter authored and pawamoy committed Sep 9, 2024
1 parent aa6c7e4 commit 344df50
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 24 deletions.
78 changes: 54 additions & 24 deletions src/_griffe/docstrings/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,22 +521,37 @@ def _read_yields_section(
docstring: Docstring,
*,
offset: int,
returns_multiple_items: bool = True,
returns_named_value: bool = True,
**options: Any,
) -> tuple[DocstringSectionYields | None, int]:
yields = []
block, new_offset = _read_block_items(docstring, offset=offset, **options)

for index, (line_number, yield_lines) in enumerate(block):
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0])
if not match:
docstring_warning(
docstring,
line_number,
f"Failed to get name, annotation or description from '{yield_lines[0]}'",
)
continue
if returns_multiple_items:
block, new_offset = _read_block_items(docstring, offset=offset, **options)
else:
one_block, new_offset = _read_block(docstring, offset=offset, **options)
block = [(new_offset, one_block.splitlines())]

name, annotation, description = match.groups()
for index, (line_number, yield_lines) in enumerate(block):
if returns_named_value:
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0])
if not match:
docstring_warning(
docstring,
line_number,
f"Failed to get name, annotation or description from '{yield_lines[0]}'",
)
continue
name, annotation, description = match.groups()
else:
name = None
if ":" in yield_lines[0]:
annotation, description = yield_lines[0].split(":", 1)
annotation = annotation.lstrip("(").rstrip(")")
else:
annotation = None
description = yield_lines[0]
description = "\n".join([description.lstrip(), *yield_lines[1:]]).rstrip("\n")

if annotation:
Expand All @@ -554,7 +569,7 @@ def _read_yields_section(
raise ValueError
if isinstance(yield_item, ExprName):
annotation = yield_item
elif yield_item.is_tuple:
elif yield_item.is_tuple and returns_multiple_items:
annotation = yield_item.slice.elements[index]
else:
annotation = yield_item
Expand All @@ -572,22 +587,37 @@ def _read_receives_section(
docstring: Docstring,
*,
offset: int,
receives_multiple_items: bool = True,
receives_named_value: bool = True,
**options: Any,
) -> tuple[DocstringSectionReceives | None, int]:
receives = []
block, new_offset = _read_block_items(docstring, offset=offset, **options)

for index, (line_number, receive_lines) in enumerate(block):
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0])
if not match:
docstring_warning(
docstring,
line_number,
f"Failed to get name, annotation or description from '{receive_lines[0]}'",
)
continue
if receives_multiple_items:
block, new_offset = _read_block_items(docstring, offset=offset, **options)
else:
one_block, new_offset = _read_block(docstring, offset=offset, **options)
block = [(new_offset, one_block.splitlines())]

name, annotation, description = match.groups()
for index, (line_number, receive_lines) in enumerate(block):
if receives_multiple_items:
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0])
if not match:
docstring_warning(
docstring,
line_number,
f"Failed to get name, annotation or description from '{receive_lines[0]}'",
)
continue
name, annotation, description = match.groups()
else:
name = None
if ":" in receive_lines[0]:
annotation, description = receive_lines[0].split(":", 1)
annotation = annotation.lstrip("(").rstrip(")")
else:
annotation = None
description = receive_lines[0]
description = "\n".join([description.lstrip(), *receive_lines[1:]]).rstrip("\n")

if annotation:
Expand All @@ -601,7 +631,7 @@ def _read_receives_section(
receives_item = annotation.slice.elements[1]
if isinstance(receives_item, ExprName):
annotation = receives_item
elif receives_item.is_tuple:
elif receives_item.is_tuple and receives_multiple_items:
annotation = receives_item.slice.elements[index]
else:
annotation = receives_item
Expand Down
218 changes: 218 additions & 0 deletions tests/test_docstrings/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
Attribute,
Class,
Docstring,
DocstringReceive,
DocstringReturn,
DocstringSectionKind,
DocstringYield,
ExprName,
Function,
Module,
Expand Down Expand Up @@ -1407,6 +1409,148 @@ def test_parse_returns_multiple_items(
assert annotated.description == expected_.description


@pytest.mark.parametrize(
("returns_multiple_items", "return_annotation", "expected"),
[
(
False,
None,
[DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)],
),
(
False,
"Iterator[tuple[int, int]]",
[DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")],
),
(
True,
None,
[
DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation=None),
DocstringYield("", description="ZZZZZZZ", annotation=None),
],
),
(
True,
"Iterator[tuple[int,int]]",
[
DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation="int"),
DocstringYield("", description="ZZZZZZZ", annotation="int"),
],
),
],
)
def test_parse_yields_multiple_items(
parse_google: ParserType,
returns_multiple_items: bool,
return_annotation: str,
expected: list[DocstringYield],
) -> None:
"""Parse Returns section with and without multiple items.
Parameters:
parse_google: Fixture parser.
returns_multiple_items: Whether the `Returns` and `Yields` sections have multiple items.
return_annotation: The return annotation of the function to parse. Usually an `Iterator`.
expected: The expected value of the parsed Yields section.
"""
parent = (
Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))))
if return_annotation is not None
else None
)
docstring = """
Yields:
XXXXXXX
YYYYYYY
ZZZZZZZ
"""
sections, _ = parse_google(
docstring,
returns_multiple_items=returns_multiple_items,
parent=parent,
)

assert len(sections) == 1
assert len(sections[0].value) == len(expected)

for annotated, expected_ in zip(sections[0].value, expected):
assert annotated.name == expected_.name
assert str(annotated.annotation) == str(expected_.annotation)
assert annotated.description == expected_.description


@pytest.mark.parametrize(
("receives_multiple_items", "return_annotation", "expected"),
[
(
False,
None,
[DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)],
),
(
False,
"Generator[..., tuple[int, int], ...]",
[DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")],
),
(
True,
None,
[
DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation=None),
DocstringReceive("", description="ZZZZZZZ", annotation=None),
],
),
(
True,
"Generator[..., tuple[int, int], ...]",
[
DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation="int"),
DocstringReceive("", description="ZZZZZZZ", annotation="int"),
],
),
],
)
def test_parse_receives_multiple_items(
parse_google: ParserType,
receives_multiple_items: bool,
return_annotation: str,
expected: list[DocstringReceive],
) -> None:
"""Parse Returns section with and without multiple items.
Parameters:
parse_google: Fixture parser.
receives_multiple_items: Whether the `Receives` section has multiple items.
return_annotation: The return annotation of the function to parse. Usually a `Generator`.
expected: The expected value of the parsed Receives section.
"""
parent = (
Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))))
if return_annotation is not None
else None
)
docstring = """
Receives:
XXXXXXX
YYYYYYY
ZZZZZZZ
"""
sections, _ = parse_google(
docstring,
receives_multiple_items=receives_multiple_items,
parent=parent,
)

assert len(sections) == 1
assert len(sections[0].value) == len(expected)

for annotated, expected_ in zip(sections[0].value, expected):
assert annotated.name == expected_.name
assert str(annotated.annotation) == str(expected_.annotation)
assert annotated.description == expected_.description


def test_avoid_false_positive_sections(parse_google: ParserType) -> None:
"""Avoid false positive when parsing sections.
Expand Down Expand Up @@ -1490,6 +1634,80 @@ def test_type_in_returns_without_parentheses(parse_google: ParserType) -> None:
assert retval.description == "Description\non several lines."


def test_type_in_yields_without_parentheses(parse_google: ParserType) -> None:
"""Assert we can parse the return type without parentheses.
Parameters:
parse_google: Fixture parser.
"""
docstring = """
Summary.
Yields:
int: Description
on several lines.
"""
sections, warnings = parse_google(docstring, returns_named_value=False)
assert len(sections) == 2
assert not warnings
retval = sections[1].value[0]
assert retval.name == ""
assert retval.annotation == "int"
assert retval.description == "Description\non several lines."

docstring = """
Summary.
Yields:
Description
on several lines.
"""
sections, warnings = parse_google(docstring, returns_named_value=False)
assert len(sections) == 2
assert len(warnings) == 1
retval = sections[1].value[0]
assert retval.name == ""
assert retval.annotation is None
assert retval.description == "Description\non several lines."


def test_type_in_receives_without_parentheses(parse_google: ParserType) -> None:
"""Assert we can parse the return type without parentheses.
Parameters:
parse_google: Fixture parser.
"""
docstring = """
Summary.
Receives:
int: Description
on several lines.
"""
sections, warnings = parse_google(docstring, receives_named_value=False)
assert len(sections) == 2
assert not warnings
retval = sections[1].value[0]
assert retval.name == ""
assert retval.annotation == "int"
assert retval.description == "Description\non several lines."

docstring = """
Summary.
Receives:
Description
on several lines.
"""
sections, warnings = parse_google(docstring, receives_named_value=False)
assert len(sections) == 2
assert len(warnings) == 1
retval = sections[1].value[0]
assert retval.name == ""
assert retval.annotation is None
assert retval.description == "Description\non several lines."


def test_reading_property_type_in_summary(parse_google: ParserType) -> None:
"""Assert we can parse the return type of properties in their summary.
Expand Down

0 comments on commit 344df50

Please sign in to comment.