Skip to content

Initial fix to switch order of preference for using docstring annotat… #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 32 additions & 27 deletions src/pytkdocs/parsers/docstrings/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,36 +232,41 @@ def _parse_parameters_section(self, lines: List[str], start_index: int) -> Tuple
block, i = self.read_block_items(lines, start_index)

for param_line in block:

# Check that there is an annotation in the docstring
try:
name_with_type, description = param_line.split(":", 1)
except ValueError:
self.error(f"Failed to get 'name: description' pair from '{param_line}'")
continue

# Setting defaults
default = empty
annotation = empty
kind = None
# Can only get description from docstring - keep if no type was given
description = description.lstrip()

# If we have managed to find a type in the docstring use this
if " " in name_with_type:
name, type_ = name_with_type.split(" ", 1)
type_ = type_.strip("()")
if type_.endswith(", optional"):
type_ = type_[:-10]
annotation = type_.strip("()")
if annotation.endswith(", optional"): # type: ignore
annotation = annotation[:-10] # type: ignore
# Otherwise try to use the signature as `annotation` would still be empty
else:
name = name_with_type
type_ = empty

default = empty
annotation = type_
kind = None

# Check in the signature to get extra details
try:
signature_param = self.context["signature"].parameters[name.lstrip("*")] # type: ignore
except (AttributeError, KeyError):
self.error(f"No type annotation for parameter '{name}'")
else:
if signature_param.annotation is not empty:
# If signature_param.X are empty it doesnt matter as defaults are empty anyway
if annotation is empty:
annotation = signature_param.annotation
if signature_param.default is not empty:
default = signature_param.default
default = signature_param.default
kind = signature_param.kind

parameters.append(
Expand Down Expand Up @@ -388,29 +393,29 @@ def read_return_section(self, lines: List[str], start_index: int) -> Tuple[Optio
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
text, i = self.read_block(lines, start_index)
annotation = self.context["annotation"]
description = ""

if self.context["signature"]:
annotation = self.context["signature"].return_annotation
else:
annotation = self.context["annotation"]

if annotation is empty:
if text:
try:
type_, text = text.split(":", 1)
except ValueError:
self.error("No type in return description")
else:
annotation = type_.lstrip()
text = text.lstrip()
# First try to get the annotation and description from the docstring
if text:
try:
type_, text = text.split(":", 1)
except ValueError:
self.error("No type in return description")
else:
self.error("No return type annotation")
annotation = type_.lstrip()
description = text.lstrip()

# If there was no annotation in the docstring then move to signature
if annotation is empty and self.context["signature"]:
annotation = self.context["signature"].return_annotation

# Early exit if there was no annotation in the docstring or the signature
if annotation is empty and not text:
self.error(f"Empty return section at line {start_index}")
return None, i

return Section(Section.Type.RETURN, AnnotatedObject(annotation, text)), i
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description)), i

def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Expand Down
46 changes: 38 additions & 8 deletions tests/test_parsers/test_docstrings/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_property_docstring():
prop = class_.attributes[0]
sections, errors = prop.docstring_sections, prop.docstring_errors
assert len(sections) == 2
assert not errors
assert len(errors) == 1


def test_function_without_annotations():
Expand Down Expand Up @@ -125,7 +125,7 @@ def f(x: int, y: int, *, z: int) -> int:

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 4
assert not errors
assert len(errors) == 1


def test_function_with_examples():
Expand Down Expand Up @@ -298,28 +298,58 @@ def f(x=1, y=None, *, z=None):


def test_types_in_signature_and_docstring():
"""Parse types in both signature and docstring."""
"""Parse types in both signature and docstring. Should prefer the docstring type"""

def f(x: int, y: int, *, z: int) -> int:
"""
The types are written both in the signature and in the docstring.

Parameters:
x (int): X value.
y (int): Y value.
x (str): X value.
y (str): Y value.

Keyword Args:
z (int): Z value.
z (str): Z value.

Returns:
int: Sum X + Y + Z.
str: Sum X + Y + Z.
"""
return x + y + z

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 4
assert not errors

assert sections[0].type == Section.Type.MARKDOWN
assert sections[1].type == Section.Type.PARAMETERS
assert sections[2].type == Section.Type.KEYWORD_ARGS
assert sections[3].type == Section.Type.RETURN

x, y = sections[1].value
(z,) = sections[2].value
r = sections[3].value

assert x.name == "x"
assert x.annotation == "str"
assert x.description == "X value."
assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
assert x.default is inspect.Signature.empty

assert y.name == "y"
assert y.annotation == "str"
assert y.description == "Y value."
assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
assert y.default is inspect.Signature.empty

assert z.name == "z"
assert z.annotation == "str"
assert z.description == "Z value."
assert z.kind is inspect.Parameter.KEYWORD_ONLY
assert z.default is inspect.Signature.empty

assert r.annotation == "str"
assert r.description == "Sum X + Y + Z."


def test_close_sections():
"""Parse sections without blank lines in between."""
Expand Down Expand Up @@ -498,7 +528,7 @@ def f():
assert len(sections) == 1
for error in errors[:3]:
assert "Empty" in error
assert "No return type" in errors[3]
assert "Empty return section at line" in errors[3]
assert "Empty" in errors[-1]


Expand Down
5 changes: 2 additions & 3 deletions tests/test_parsers/test_docstrings/test_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class DummyObject:
def parse(docstring, signature=None, return_type=inspect.Signature.empty):
"""Helper to parse a doctring."""
return Numpy().parse(
dedent(docstring).strip(),
{"obj": DummyObject(), "signature": signature, "type": return_type},
dedent(docstring).strip(), {"obj": DummyObject(), "signature": signature, "type": return_type},
)


Expand Down Expand Up @@ -80,7 +79,7 @@ def test_property_docstring():
prop = class_.attributes[0]
sections, errors = prop.docstring_sections, prop.docstring_errors
assert len(sections) == 2
assert not errors
assert len(errors) == 1


def test_function_without_annotations():
Expand Down