Skip to content

Commit

Permalink
feat: Retrieve annotations from parent in Numpy parser
Browse files Browse the repository at this point in the history
Implemented for the Attributes, Returns, Yields and Receives sections.

Issue #29: #29
Issue #30: #30
Issue #31: #31
Issue #32: #32
  • Loading branch information
pawamoy committed Apr 3, 2022
1 parent 603dc0e commit 8d4eae3
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 51 deletions.
29 changes: 12 additions & 17 deletions docs/docstrings.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,18 @@ Yields | ✅ | ✅ | [❌][issue-section-sphinx-yields]

## Getting annotations/defaults from parent

Section | Google | Numpy | Sphinx
---------------- | ------ | ----------------------------------- | ------
Attributes | ✅ | [][issue-parent-numpy-attributes] | [][issue-parent-sphinx-attributes]
Deprecated | / | / | /
Examples | / | / | /
Other Parameters | ✅ | ✅ | [][issue-parent-sphinx-other-parameters]
Parameters | ✅ | ✅ | ✅
Raises | / | / | /
Receives | ✅ | [][issue-parent-numpy-receives] | [][issue-parent-sphinx-receives]
Returns | ✅ | [][issue-parent-numpy-returns] | ✅
Warns | / | / | /
Yields | ✅ | [][issue-parent-numpy-yields] | [][issue-parent-sphinx-yields]

[issue-parent-numpy-attributes]: https://github.com/mkdocstrings/griffe/issues/29
[issue-parent-numpy-receives]: https://github.com/mkdocstrings/griffe/issues/30
[issue-parent-numpy-returns]: https://github.com/mkdocstrings/griffe/issues/31
[issue-parent-numpy-yields]: https://github.com/mkdocstrings/griffe/issues/32
Section | Google | Numpy | Sphinx
---------------- | ------ | ----- | ------
Attributes | ✅ | ✅ | [][issue-parent-sphinx-attributes]
Deprecated | / | / | /
Examples | / | / | /
Other Parameters | ✅ | ✅ | [][issue-parent-sphinx-other-parameters]
Parameters | ✅ | ✅ | ✅
Raises | / | / | /
Receives | ✅ | ✅ | [][issue-parent-sphinx-receives]
Returns | ✅ | ✅ | ✅
Warns | / | / | /
Yields | ✅ | ✅ | [][issue-parent-sphinx-yields]

[issue-parent-sphinx-attributes]: https://github.com/mkdocstrings/griffe/issues/33
[issue-parent-sphinx-other-parameters]: https://github.com/mkdocstrings/griffe/issues/34
Expand Down
64 changes: 59 additions & 5 deletions src/griffe/docstrings/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,14 +326,36 @@ def _read_returns_section( # noqa: WPS231
return None, new_offset

returns = []
for item in items:
for index, item in enumerate(items):
match = _RE_RETURNS.match(item[0])
if not match:
_warn(docstring, new_offset, f"Could not parse line '{item[0]}'")
continue

name, annotation = match.groups()
annotation = annotation or None
text = dedent("\n".join(item[1:]))
if annotation is None:
# try to retrieve the annotation from the docstring parent
with suppress(AttributeError, KeyError, ValueError):
annotation = docstring.parent.returns # type: ignore[union-attr]
if len(items) > 1:
if annotation.is_tuple:
annotation = annotation.tuple_item(index)
else:
if annotation.is_iterator:
return_item = annotation.iterator_item()
elif annotation.is_generator:
_, _, return_item = annotation.generator_items()
else:
raise ValueError
if isinstance(return_item, Name):
annotation = return_item
elif return_item.is_tuple:
annotation = return_item.tuple_item(index)
else:
annotation = return_item

returns.append(DocstringReturn(name=name or "", annotation=annotation, description=text))
return DocstringSectionReturns(returns), new_offset

Expand All @@ -353,14 +375,32 @@ def _read_yields_section( # noqa: WPS231
return None, new_offset

yields = []
for item in items:
for index, item in enumerate(items):
match = _RE_YIELDS.match(item[0])
if not match:
_warn(docstring, new_offset, f"Could not parse line '{item[0]}'")
continue

name, annotation = match.groups()
annotation = annotation or None
text = dedent("\n".join(item[1:]))
if annotation is None:
# try to retrieve the annotation from the docstring parent
with suppress(AttributeError, KeyError, ValueError):
annotation = docstring.parent.returns # type: ignore[union-attr]
if len(items) > 1:
if annotation.is_iterator:
yield_item = annotation.iterator_item()
elif annotation.is_generator:
yield_item, _, _ = annotation.generator_items()
else:
raise ValueError
if isinstance(yield_item, Name):
annotation = yield_item
elif yield_item.is_tuple:
annotation = yield_item.tuple_item(index)
else:
annotation = yield_item
yields.append(DocstringYield(name=name or "", annotation=annotation, description=text))
return DocstringSectionYields(yields), new_offset

Expand All @@ -380,14 +420,27 @@ def _read_receives_section( # noqa: WPS231
return None, new_offset

receives = []
for item in items:
for index, item in enumerate(items):
match = _RE_RECEIVES.match(item[0])
if not match:
_warn(docstring, new_offset, f"Could not parse line '{item[0]}'")
continue

name, annotation = match.groups()
annotation = annotation or None
text = dedent("\n".join(item[1:]))
if annotation is None:
# try to retrieve the annotation from the docstring parent
with suppress(AttributeError, KeyError):
annotation = docstring.parent.returns # type: ignore[union-attr]
if len(items) > 1 and annotation.is_generator:
_, receives_item, _ = annotation.generator_items()
if isinstance(receives_item, Name):
annotation = receives_item
elif receives_item.is_tuple:
annotation = receives_item.tuple_item(index)
else:
annotation = receives_item
receives.append(DocstringReceive(name=name or "", annotation=annotation, description=text))
return DocstringSectionReceives(receives), new_offset

Expand Down Expand Up @@ -450,15 +503,16 @@ def _read_attributes_section(
_warn(docstring, new_offset, f"Empty attributes section at line {offset}")
return None, new_offset

annotation: str | None
annotation: str | Name | Expression | None = None
attributes = []
for item in items:
name_type = item[0]
if " : " in name_type:
name, annotation = name_type.split(" : ", 1)
else:
name = name_type
annotation = None
with suppress(AttributeError, KeyError):
annotation = docstring.parent.members[name].annotation # type: ignore[union-attr]
text = dedent("\n".join(item[1:]))
attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text))
return DocstringSectionAttributes(attributes), new_offset
Expand Down
193 changes: 164 additions & 29 deletions tests/test_docstrings/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from griffe.dataclasses import Function, Parameter, Parameters
from griffe.dataclasses import Attribute, Class, Docstring, Function, Parameter, Parameters
from griffe.docstrings.dataclasses import (
DocstringAttribute,
DocstringParameter,
Expand All @@ -15,6 +15,8 @@
DocstringWarn,
DocstringYield,
)
from griffe.docstrings.utils import parse_annotation
from griffe.expressions import Name
from tests.test_docstrings.helpers import assert_attribute_equal, assert_element_equal, assert_parameter_equal


Expand Down Expand Up @@ -95,7 +97,56 @@ def test_indented_code_block(parse_numpy):


# =============================================================================================
# Sections
# Annotations (general)
def test_prefer_docstring_type_over_annotation(parse_numpy):
"""Prefer the type written in the docstring over the annotation in the parent.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Parameters
----------
a : int
"""

sections, _ = parse_numpy(
docstring, parent=Function("func", parameters=Parameters(Parameter("a", annotation="str")))
)
assert len(sections) == 1
assert_parameter_equal(sections[0].value[0], DocstringParameter("a", description="", annotation="int"))


def test_parse_complex_annotations(parse_numpy):
"""Check the type regex accepts all the necessary characters.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Parameters
----------
a : typing.Tuple[str, random0123456789]
b : int | float | None
c : Literal['hello'] | Literal["world"]
"""

sections, _ = parse_numpy(docstring)
assert len(sections) == 1
param_a, param_b, param_c = sections[0].value
assert param_a.name == "a"
assert param_a.description == ""
assert param_a.annotation == "typing.Tuple[str, random0123456789]"
assert param_b.name == "b"
assert param_b.description == ""
assert param_b.annotation == "int | float | None"
assert param_c.name == "c"
assert param_c.description == ""
assert param_c.annotation == "Literal['hello'] | Literal[\"world\"]"


# =============================================================================================
# Sections (general)
def test_parameters_section(parse_numpy):
"""Parse parameters section.
Expand Down Expand Up @@ -405,52 +456,136 @@ def test_examples_section_when_followed_by_named_section(parse_numpy):


# =============================================================================================
# Annotations
def test_prefer_docstring_type_over_annotation(parse_numpy):
"""Prefer the type written in the docstring over the annotation in the parent.
# Attributes sections
def test_retrieve_attributes_annotation_from_parent(parse_numpy):
"""Retrieve the annotations of attributes from the parent object.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Parameters
Summary.
Attributes
----------
a : int
a :
Whatever.
b :
Whatever.
"""
parent = Class("cls")
parent["a"] = Attribute("a", annotation=Name("int", "int"))
parent["b"] = Attribute("b", annotation=Name("str", "str"))
sections, _ = parse_numpy(docstring, parent=parent)
attributes = sections[1].value
assert attributes[0].name == "a"
assert attributes[0].annotation.source == "int"
assert attributes[1].name == "b"
assert attributes[1].annotation.source == "str"


# =============================================================================================
# Yields sections
@pytest.mark.parametrize(
"return_annotation",
[
"Iterator[tuple[int, float]]",
"Generator[tuple[int, float], ..., ...]",
],
)
def test_parse_yields_tuple_in_iterator_or_generator(parse_numpy, return_annotation):
"""Parse Yields annotations in Iterator or Generator types.
Parameters:
parse_numpy: Fixture parser.
return_annotation: Parametrized return annotation as a string.
"""
docstring = """
Summary.
Yields
------
a :
Whatever.
b :
Whatever.
"""
sections, _ = parse_numpy(
docstring, parent=Function("func", parameters=Parameters(Parameter("a", annotation="str")))
docstring,
parent=Function(
"func",
returns=parse_annotation(return_annotation, Docstring("d", parent=Function("f"))),
),
)
assert len(sections) == 1
assert_parameter_equal(sections[0].value[0], DocstringParameter("a", description="", annotation="int"))
yields = sections[1].value
assert yields[0].name == "a"
assert yields[0].annotation.source == "int"
assert yields[1].name == "b"
assert yields[1].annotation.source == "float"


def test_parse_complex_annotations(parse_numpy):
"""Check the type regex accepts all the necessary characters.
# =============================================================================================
# Receives sections
def test_parse_receives_tuple_in_generator(parse_numpy):
"""Parse Receives annotations in Generator type.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Parameters
----------
a : typing.Tuple[str, random0123456789]
b : int | float | None
c : Literal['hello'] | Literal["world"]
Summary.
Receives
--------
a :
Whatever.
b :
Whatever.
"""
sections, _ = parse_numpy(
docstring,
parent=Function(
"func",
returns=parse_annotation("Generator[..., tuple[int, float], ...]", Docstring("d", parent=Function("f"))),
),
)
receives = sections[1].value
assert receives[0].name == "a"
assert receives[0].annotation.source == "int"
assert receives[1].name == "b"
assert receives[1].annotation.source == "float"

sections, _ = parse_numpy(docstring)
assert len(sections) == 1
param_a, param_b, param_c = sections[0].value
assert param_a.name == "a"
assert param_a.description == ""
assert param_a.annotation == "typing.Tuple[str, random0123456789]"
assert param_b.name == "b"
assert param_b.description == ""
assert param_b.annotation == "int | float | None"
assert param_c.name == "c"
assert param_c.description == ""
assert param_c.annotation == "Literal['hello'] | Literal[\"world\"]"

# =============================================================================================
# Returns sections
def test_parse_returns_tuple_in_generator(parse_numpy):
"""Parse Returns annotations in Generator type.
Parameters:
parse_numpy: Fixture parser.
"""
docstring = """
Summary.
Returns
-------
a :
Whatever.
b :
Whatever.
"""
sections, _ = parse_numpy(
docstring,
parent=Function(
"func",
returns=parse_annotation("Generator[..., ..., tuple[int, float]]", Docstring("d", parent=Function("f"))),
),
)
returns = sections[1].value
assert returns[0].name == "a"
assert returns[0].annotation.source == "int"
assert returns[1].name == "b"
assert returns[1].annotation.source == "float"


# =============================================================================================
Expand Down

0 comments on commit 8d4eae3

Please sign in to comment.