Skip to content

Commit 344df50

Browse files
the-13th-letterpawamoy
authored andcommitted
feat: Allow deselecting multiple or named items in Yields and Receives
Issue-263: #263
1 parent aa6c7e4 commit 344df50

File tree

2 files changed

+272
-24
lines changed

2 files changed

+272
-24
lines changed

src/_griffe/docstrings/google.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -521,22 +521,37 @@ def _read_yields_section(
521521
docstring: Docstring,
522522
*,
523523
offset: int,
524+
returns_multiple_items: bool = True,
525+
returns_named_value: bool = True,
524526
**options: Any,
525527
) -> tuple[DocstringSectionYields | None, int]:
526528
yields = []
527-
block, new_offset = _read_block_items(docstring, offset=offset, **options)
528529

529-
for index, (line_number, yield_lines) in enumerate(block):
530-
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0])
531-
if not match:
532-
docstring_warning(
533-
docstring,
534-
line_number,
535-
f"Failed to get name, annotation or description from '{yield_lines[0]}'",
536-
)
537-
continue
530+
if returns_multiple_items:
531+
block, new_offset = _read_block_items(docstring, offset=offset, **options)
532+
else:
533+
one_block, new_offset = _read_block(docstring, offset=offset, **options)
534+
block = [(new_offset, one_block.splitlines())]
538535

539-
name, annotation, description = match.groups()
536+
for index, (line_number, yield_lines) in enumerate(block):
537+
if returns_named_value:
538+
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0])
539+
if not match:
540+
docstring_warning(
541+
docstring,
542+
line_number,
543+
f"Failed to get name, annotation or description from '{yield_lines[0]}'",
544+
)
545+
continue
546+
name, annotation, description = match.groups()
547+
else:
548+
name = None
549+
if ":" in yield_lines[0]:
550+
annotation, description = yield_lines[0].split(":", 1)
551+
annotation = annotation.lstrip("(").rstrip(")")
552+
else:
553+
annotation = None
554+
description = yield_lines[0]
540555
description = "\n".join([description.lstrip(), *yield_lines[1:]]).rstrip("\n")
541556

542557
if annotation:
@@ -554,7 +569,7 @@ def _read_yields_section(
554569
raise ValueError
555570
if isinstance(yield_item, ExprName):
556571
annotation = yield_item
557-
elif yield_item.is_tuple:
572+
elif yield_item.is_tuple and returns_multiple_items:
558573
annotation = yield_item.slice.elements[index]
559574
else:
560575
annotation = yield_item
@@ -572,22 +587,37 @@ def _read_receives_section(
572587
docstring: Docstring,
573588
*,
574589
offset: int,
590+
receives_multiple_items: bool = True,
591+
receives_named_value: bool = True,
575592
**options: Any,
576593
) -> tuple[DocstringSectionReceives | None, int]:
577594
receives = []
578-
block, new_offset = _read_block_items(docstring, offset=offset, **options)
579595

580-
for index, (line_number, receive_lines) in enumerate(block):
581-
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0])
582-
if not match:
583-
docstring_warning(
584-
docstring,
585-
line_number,
586-
f"Failed to get name, annotation or description from '{receive_lines[0]}'",
587-
)
588-
continue
596+
if receives_multiple_items:
597+
block, new_offset = _read_block_items(docstring, offset=offset, **options)
598+
else:
599+
one_block, new_offset = _read_block(docstring, offset=offset, **options)
600+
block = [(new_offset, one_block.splitlines())]
589601

590-
name, annotation, description = match.groups()
602+
for index, (line_number, receive_lines) in enumerate(block):
603+
if receives_multiple_items:
604+
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0])
605+
if not match:
606+
docstring_warning(
607+
docstring,
608+
line_number,
609+
f"Failed to get name, annotation or description from '{receive_lines[0]}'",
610+
)
611+
continue
612+
name, annotation, description = match.groups()
613+
else:
614+
name = None
615+
if ":" in receive_lines[0]:
616+
annotation, description = receive_lines[0].split(":", 1)
617+
annotation = annotation.lstrip("(").rstrip(")")
618+
else:
619+
annotation = None
620+
description = receive_lines[0]
591621
description = "\n".join([description.lstrip(), *receive_lines[1:]]).rstrip("\n")
592622

593623
if annotation:
@@ -601,7 +631,7 @@ def _read_receives_section(
601631
receives_item = annotation.slice.elements[1]
602632
if isinstance(receives_item, ExprName):
603633
annotation = receives_item
604-
elif receives_item.is_tuple:
634+
elif receives_item.is_tuple and receives_multiple_items:
605635
annotation = receives_item.slice.elements[index]
606636
else:
607637
annotation = receives_item

tests/test_docstrings/test_google.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
Attribute,
1212
Class,
1313
Docstring,
14+
DocstringReceive,
1415
DocstringReturn,
1516
DocstringSectionKind,
17+
DocstringYield,
1618
ExprName,
1719
Function,
1820
Module,
@@ -1407,6 +1409,148 @@ def test_parse_returns_multiple_items(
14071409
assert annotated.description == expected_.description
14081410

14091411

1412+
@pytest.mark.parametrize(
1413+
("returns_multiple_items", "return_annotation", "expected"),
1414+
[
1415+
(
1416+
False,
1417+
None,
1418+
[DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)],
1419+
),
1420+
(
1421+
False,
1422+
"Iterator[tuple[int, int]]",
1423+
[DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")],
1424+
),
1425+
(
1426+
True,
1427+
None,
1428+
[
1429+
DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation=None),
1430+
DocstringYield("", description="ZZZZZZZ", annotation=None),
1431+
],
1432+
),
1433+
(
1434+
True,
1435+
"Iterator[tuple[int,int]]",
1436+
[
1437+
DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation="int"),
1438+
DocstringYield("", description="ZZZZZZZ", annotation="int"),
1439+
],
1440+
),
1441+
],
1442+
)
1443+
def test_parse_yields_multiple_items(
1444+
parse_google: ParserType,
1445+
returns_multiple_items: bool,
1446+
return_annotation: str,
1447+
expected: list[DocstringYield],
1448+
) -> None:
1449+
"""Parse Returns section with and without multiple items.
1450+
1451+
Parameters:
1452+
parse_google: Fixture parser.
1453+
returns_multiple_items: Whether the `Returns` and `Yields` sections have multiple items.
1454+
return_annotation: The return annotation of the function to parse. Usually an `Iterator`.
1455+
expected: The expected value of the parsed Yields section.
1456+
"""
1457+
parent = (
1458+
Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))))
1459+
if return_annotation is not None
1460+
else None
1461+
)
1462+
docstring = """
1463+
Yields:
1464+
XXXXXXX
1465+
YYYYYYY
1466+
ZZZZZZZ
1467+
"""
1468+
sections, _ = parse_google(
1469+
docstring,
1470+
returns_multiple_items=returns_multiple_items,
1471+
parent=parent,
1472+
)
1473+
1474+
assert len(sections) == 1
1475+
assert len(sections[0].value) == len(expected)
1476+
1477+
for annotated, expected_ in zip(sections[0].value, expected):
1478+
assert annotated.name == expected_.name
1479+
assert str(annotated.annotation) == str(expected_.annotation)
1480+
assert annotated.description == expected_.description
1481+
1482+
1483+
@pytest.mark.parametrize(
1484+
("receives_multiple_items", "return_annotation", "expected"),
1485+
[
1486+
(
1487+
False,
1488+
None,
1489+
[DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)],
1490+
),
1491+
(
1492+
False,
1493+
"Generator[..., tuple[int, int], ...]",
1494+
[DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")],
1495+
),
1496+
(
1497+
True,
1498+
None,
1499+
[
1500+
DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation=None),
1501+
DocstringReceive("", description="ZZZZZZZ", annotation=None),
1502+
],
1503+
),
1504+
(
1505+
True,
1506+
"Generator[..., tuple[int, int], ...]",
1507+
[
1508+
DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation="int"),
1509+
DocstringReceive("", description="ZZZZZZZ", annotation="int"),
1510+
],
1511+
),
1512+
],
1513+
)
1514+
def test_parse_receives_multiple_items(
1515+
parse_google: ParserType,
1516+
receives_multiple_items: bool,
1517+
return_annotation: str,
1518+
expected: list[DocstringReceive],
1519+
) -> None:
1520+
"""Parse Returns section with and without multiple items.
1521+
1522+
Parameters:
1523+
parse_google: Fixture parser.
1524+
receives_multiple_items: Whether the `Receives` section has multiple items.
1525+
return_annotation: The return annotation of the function to parse. Usually a `Generator`.
1526+
expected: The expected value of the parsed Receives section.
1527+
"""
1528+
parent = (
1529+
Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))))
1530+
if return_annotation is not None
1531+
else None
1532+
)
1533+
docstring = """
1534+
Receives:
1535+
XXXXXXX
1536+
YYYYYYY
1537+
ZZZZZZZ
1538+
"""
1539+
sections, _ = parse_google(
1540+
docstring,
1541+
receives_multiple_items=receives_multiple_items,
1542+
parent=parent,
1543+
)
1544+
1545+
assert len(sections) == 1
1546+
assert len(sections[0].value) == len(expected)
1547+
1548+
for annotated, expected_ in zip(sections[0].value, expected):
1549+
assert annotated.name == expected_.name
1550+
assert str(annotated.annotation) == str(expected_.annotation)
1551+
assert annotated.description == expected_.description
1552+
1553+
14101554
def test_avoid_false_positive_sections(parse_google: ParserType) -> None:
14111555
"""Avoid false positive when parsing sections.
14121556
@@ -1490,6 +1634,80 @@ def test_type_in_returns_without_parentheses(parse_google: ParserType) -> None:
14901634
assert retval.description == "Description\non several lines."
14911635

14921636

1637+
def test_type_in_yields_without_parentheses(parse_google: ParserType) -> None:
1638+
"""Assert we can parse the return type without parentheses.
1639+
1640+
Parameters:
1641+
parse_google: Fixture parser.
1642+
"""
1643+
docstring = """
1644+
Summary.
1645+
1646+
Yields:
1647+
int: Description
1648+
on several lines.
1649+
"""
1650+
sections, warnings = parse_google(docstring, returns_named_value=False)
1651+
assert len(sections) == 2
1652+
assert not warnings
1653+
retval = sections[1].value[0]
1654+
assert retval.name == ""
1655+
assert retval.annotation == "int"
1656+
assert retval.description == "Description\non several lines."
1657+
1658+
docstring = """
1659+
Summary.
1660+
1661+
Yields:
1662+
Description
1663+
on several lines.
1664+
"""
1665+
sections, warnings = parse_google(docstring, returns_named_value=False)
1666+
assert len(sections) == 2
1667+
assert len(warnings) == 1
1668+
retval = sections[1].value[0]
1669+
assert retval.name == ""
1670+
assert retval.annotation is None
1671+
assert retval.description == "Description\non several lines."
1672+
1673+
1674+
def test_type_in_receives_without_parentheses(parse_google: ParserType) -> None:
1675+
"""Assert we can parse the return type without parentheses.
1676+
1677+
Parameters:
1678+
parse_google: Fixture parser.
1679+
"""
1680+
docstring = """
1681+
Summary.
1682+
1683+
Receives:
1684+
int: Description
1685+
on several lines.
1686+
"""
1687+
sections, warnings = parse_google(docstring, receives_named_value=False)
1688+
assert len(sections) == 2
1689+
assert not warnings
1690+
retval = sections[1].value[0]
1691+
assert retval.name == ""
1692+
assert retval.annotation == "int"
1693+
assert retval.description == "Description\non several lines."
1694+
1695+
docstring = """
1696+
Summary.
1697+
1698+
Receives:
1699+
Description
1700+
on several lines.
1701+
"""
1702+
sections, warnings = parse_google(docstring, receives_named_value=False)
1703+
assert len(sections) == 2
1704+
assert len(warnings) == 1
1705+
retval = sections[1].value[0]
1706+
assert retval.name == ""
1707+
assert retval.annotation is None
1708+
assert retval.description == "Description\non several lines."
1709+
1710+
14931711
def test_reading_property_type_in_summary(parse_google: ParserType) -> None:
14941712
"""Assert we can parse the return type of properties in their summary.
14951713

0 commit comments

Comments
 (0)