diff --git a/src/_griffe/docstrings/google.py b/src/_griffe/docstrings/google.py index db3b9b37..85539910 100644 --- a/src/_griffe/docstrings/google.py +++ b/src/_griffe/docstrings/google.py @@ -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: @@ -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 @@ -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: @@ -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 diff --git a/tests/test_docstrings/test_google.py b/tests/test_docstrings/test_google.py index 4dae96c5..15037b22 100644 --- a/tests/test_docstrings/test_google.py +++ b/tests/test_docstrings/test_google.py @@ -11,8 +11,10 @@ Attribute, Class, Docstring, + DocstringReceive, DocstringReturn, DocstringSectionKind, + DocstringYield, ExprName, Function, Module, @@ -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. @@ -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.