Skip to content

Commit c768356

Browse files
the-13th-letterpawamoy
authored andcommitted
refactor: Extract common functionality in Returns, Yields and Receives parsing
The Returns, Yields and Receives section parsers only really differ in their fallbacks and the names of their configuration settings, not so much in their general parsing behavior and the expected formatting of the section contents. This commit is an attempt to factor out the following functionality: * Read the section contents as a single block, or as multiple blocks, depending on the `multiple` setting. * Parse each block's first line as a named parameter, or an unnamed parameter, depending on the `named` setting. * Unpack `Generator` and `Iterator` types in the return annotation. Optionally error out if the return annotation is not of these types. * Optionally destructure the return tuple if `multiple` is in effect. Issue-263: #263
1 parent 344df50 commit c768356

File tree

1 file changed

+130
-108
lines changed

1 file changed

+130
-108
lines changed

src/_griffe/docstrings/google.py

Lines changed: 130 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,76 @@ def _read_warns_section(
442442
return DocstringSectionWarns(warns), new_offset
443443

444444

445+
def _read_block_items_maybe(
446+
docstring: Docstring,
447+
*,
448+
offset: int,
449+
multiple: bool = True,
450+
**options: Any,
451+
) -> _ItemsBlock:
452+
if multiple:
453+
return _read_block_items(docstring, offset=offset, **options)
454+
one_block, new_offset = _read_block(docstring, offset=offset, **options)
455+
return [(new_offset, one_block.splitlines())], new_offset
456+
457+
458+
def _get_name_annotation_description(
459+
docstring: Docstring,
460+
line_number: int,
461+
lines: list[str],
462+
*,
463+
named: bool = True,
464+
) -> tuple[str | None, Any, str]:
465+
if named:
466+
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0])
467+
if not match:
468+
docstring_warning(
469+
docstring,
470+
line_number,
471+
f"Failed to get name, annotation or description from '{lines[0]}'",
472+
)
473+
raise ValueError
474+
name, annotation, description = match.groups()
475+
else:
476+
name = None
477+
if ":" in lines[0]:
478+
annotation, description = lines[0].split(":", 1)
479+
annotation = annotation.lstrip("(").rstrip(")")
480+
else:
481+
annotation = None
482+
description = lines[0]
483+
description = "\n".join([description.lstrip(), *lines[1:]]).rstrip("\n")
484+
return name, annotation, description
485+
486+
487+
def _unpack_generators(
488+
annotation: Any,
489+
generator_pos: int,
490+
*,
491+
mandatory: bool = False,
492+
) -> Any:
493+
if annotation.is_generator:
494+
return annotation.slice.elements[generator_pos]
495+
if annotation.is_iterator:
496+
return annotation.slice
497+
if mandatory:
498+
raise ValueError(f"must be a Generator: {annotation!r}")
499+
return annotation
500+
501+
502+
def _maybe_destructure_annotation(
503+
annotation: Any,
504+
index: int,
505+
*,
506+
multiple: bool = True,
507+
) -> Any:
508+
if isinstance(annotation, ExprName):
509+
return annotation
510+
if multiple and annotation.is_tuple:
511+
return annotation.slice.elements[index]
512+
return annotation
513+
514+
445515
def _read_returns_section(
446516
docstring: Docstring,
447517
*,
@@ -452,32 +522,23 @@ def _read_returns_section(
452522
) -> tuple[DocstringSectionReturns | None, int]:
453523
returns = []
454524

455-
if returns_multiple_items:
456-
block, new_offset = _read_block_items(docstring, offset=offset, **options)
457-
else:
458-
one_block, new_offset = _read_block(docstring, offset=offset, **options)
459-
block = [(new_offset, one_block.splitlines())]
525+
block, new_offset = _read_block_items_maybe(
526+
docstring,
527+
offset=offset,
528+
multiple=returns_multiple_items,
529+
**options,
530+
)
460531

461532
for index, (line_number, return_lines) in enumerate(block):
462-
if returns_named_value:
463-
match = _RE_NAME_ANNOTATION_DESCRIPTION.match(return_lines[0])
464-
if not match:
465-
docstring_warning(
466-
docstring,
467-
line_number,
468-
f"Failed to get name, annotation or description from '{return_lines[0]}'",
469-
)
470-
continue
471-
name, annotation, description = match.groups()
472-
else:
473-
name = None
474-
if ":" in return_lines[0]:
475-
annotation, description = return_lines[0].split(":", 1)
476-
annotation = annotation.lstrip("(").rstrip(")")
477-
else:
478-
annotation = None
479-
description = return_lines[0]
480-
description = "\n".join([description.lstrip(), *return_lines[1:]]).rstrip("\n")
533+
try:
534+
name, annotation, description = _get_name_annotation_description(
535+
docstring,
536+
line_number,
537+
return_lines,
538+
named=returns_named_value,
539+
)
540+
except ValueError:
541+
continue
481542

482543
if annotation:
483544
# try to compile the annotation to transform it into an expression
@@ -491,22 +552,11 @@ def _read_returns_section(
491552
annotation = docstring.parent.annotation # type: ignore[union-attr]
492553
else:
493554
raise ValueError
494-
if len(block) > 1:
495-
if annotation.is_tuple:
496-
annotation = annotation.slice.elements[index]
497-
else:
498-
if annotation.is_iterator:
499-
return_item = annotation.slice
500-
elif annotation.is_generator:
501-
return_item = annotation.slice.elements[2]
502-
else:
503-
raise ValueError
504-
if isinstance(return_item, ExprName):
505-
annotation = return_item
506-
elif return_item.is_tuple:
507-
annotation = return_item.slice.elements[index]
508-
else:
509-
annotation = return_item
555+
annotation = _maybe_destructure_annotation(
556+
_unpack_generators(annotation, 2),
557+
index,
558+
multiple=returns_multiple_items,
559+
)
510560

511561
if annotation is None:
512562
returned_value = repr(name) if name else index + 1
@@ -527,32 +577,23 @@ def _read_yields_section(
527577
) -> tuple[DocstringSectionYields | None, int]:
528578
yields = []
529579

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())]
580+
block, new_offset = _read_block_items_maybe(
581+
docstring,
582+
offset=offset,
583+
multiple=returns_multiple_items,
584+
**options,
585+
)
535586

536587
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]
555-
description = "\n".join([description.lstrip(), *yield_lines[1:]]).rstrip("\n")
588+
try:
589+
name, annotation, description = _get_name_annotation_description(
590+
docstring,
591+
line_number,
592+
yield_lines,
593+
named=returns_named_value,
594+
)
595+
except ValueError:
596+
continue
556597

557598
if annotation:
558599
# try to compile the annotation to transform it into an expression
@@ -561,18 +602,11 @@ def _read_yields_section(
561602
# try to retrieve the annotation from the docstring parent
562603
with suppress(AttributeError, IndexError, KeyError, ValueError):
563604
annotation = docstring.parent.annotation # type: ignore[union-attr]
564-
if annotation.is_iterator:
565-
yield_item = annotation.slice
566-
elif annotation.is_generator:
567-
yield_item = annotation.slice.elements[0]
568-
else:
569-
raise ValueError
570-
if isinstance(yield_item, ExprName):
571-
annotation = yield_item
572-
elif yield_item.is_tuple and returns_multiple_items:
573-
annotation = yield_item.slice.elements[index]
574-
else:
575-
annotation = yield_item
605+
annotation = _maybe_destructure_annotation(
606+
_unpack_generators(annotation, 0, mandatory=True),
607+
index,
608+
multiple=returns_multiple_items,
609+
)
576610

577611
if annotation is None:
578612
yielded_value = repr(name) if name else index + 1
@@ -593,32 +627,23 @@ def _read_receives_section(
593627
) -> tuple[DocstringSectionReceives | None, int]:
594628
receives = []
595629

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())]
630+
block, new_offset = _read_block_items_maybe(
631+
docstring,
632+
offset=offset,
633+
multiple=receives_multiple_items,
634+
**options,
635+
)
601636

602637
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]
621-
description = "\n".join([description.lstrip(), *receive_lines[1:]]).rstrip("\n")
638+
try:
639+
name, annotation, description = _get_name_annotation_description(
640+
docstring,
641+
line_number,
642+
receive_lines,
643+
named=receives_named_value,
644+
)
645+
except ValueError:
646+
continue
622647

623648
if annotation:
624649
# try to compile the annotation to transform it into an expression
@@ -627,14 +652,11 @@ def _read_receives_section(
627652
# try to retrieve the annotation from the docstring parent
628653
with suppress(AttributeError, KeyError):
629654
annotation = docstring.parent.returns # type: ignore[union-attr]
630-
if annotation.is_generator:
631-
receives_item = annotation.slice.elements[1]
632-
if isinstance(receives_item, ExprName):
633-
annotation = receives_item
634-
elif receives_item.is_tuple and receives_multiple_items:
635-
annotation = receives_item.slice.elements[index]
636-
else:
637-
annotation = receives_item
655+
annotation = _maybe_destructure_annotation(
656+
_unpack_generators(annotation, 1, mandatory=True),
657+
index,
658+
multiple=receives_multiple_items,
659+
)
638660

639661
if annotation is None:
640662
received_value = repr(name) if name else index + 1

0 commit comments

Comments
 (0)