Skip to content

Commit

Permalink
feat: Support "Keyword Args" sections for Gooogle-style
Browse files Browse the repository at this point in the history
Add support for "Keyword Args" and "Keyword Arguments" sections for
Google-style docstrings.

Issue #88: #88
PR #105: #105
HacKanCuBa authored May 12, 2021
1 parent 8e1b1b2 commit 0133369
Showing 3 changed files with 149 additions and 24 deletions.
1 change: 1 addition & 0 deletions src/pytkdocs/parsers/docstrings/base.py
Original file line number Diff line number Diff line change
@@ -107,6 +107,7 @@ class Type:
RETURN = "return"
EXAMPLES = "examples"
ATTRIBUTES = "attributes"
KEYWORD_ARGS = "keyword_args"

def __init__(self, section_type: str, value: Any) -> None:
"""
41 changes: 39 additions & 2 deletions src/pytkdocs/parsers/docstrings/google.py
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@
"arguments:": Section.Type.PARAMETERS,
"params:": Section.Type.PARAMETERS,
"parameters:": Section.Type.PARAMETERS,
"keyword args:": Section.Type.KEYWORD_ARGS,
"keyword arguments:": Section.Type.KEYWORD_ARGS,
"raise:": Section.Type.EXCEPTIONS,
"raises:": Section.Type.EXCEPTIONS,
"except:": Section.Type.EXCEPTIONS,
@@ -39,6 +41,7 @@ def __init__(self, replace_admonitions: bool = True) -> None:
self.replace_admonitions = replace_admonitions
self.section_reader = {
Section.Type.PARAMETERS: self.read_parameters_section,
Section.Type.KEYWORD_ARGS: self.read_keyword_arguments_section,
Section.Type.EXCEPTIONS: self.read_exceptions_section,
Section.Type.EXAMPLES: self.read_examples_section,
Section.Type.ATTRIBUTES: self.read_attributes_section,
@@ -213,9 +216,9 @@ def read_block(self, lines: List[str], start_index: int) -> Tuple[str, int]:

return "\n".join(block).rstrip("\n"), i - 1

def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
def _parse_parameters_section(self, lines: List[str], start_index: int) -> Tuple[List[Parameter], int]:
"""
Parse a "parameters" section.
Parse a "parameters" or "keyword args" section.
Arguments:
lines: The parameters block lines.
@@ -265,12 +268,46 @@ def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[O
Parameter(name=name, annotation=annotation, description=description, default=default, kind=kind)
)

return parameters, i

def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "parameters" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)

if parameters:
return Section(Section.Type.PARAMETERS, parameters), i

self.error(f"Empty parameters section at line {start_index}")
return None, i

def read_keyword_arguments_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse a "keyword arguments" section.
Arguments:
lines: The parameters block lines.
start_index: The line number to start at.
Returns:
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
"""
parameters, i = self._parse_parameters_section(lines, start_index)

if parameters:
return Section(Section.Type.KEYWORD_ARGS, parameters), i

self.error(f"Empty keyword arguments section at line {start_index}")
return None, i

def read_attributes_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
"""
Parse an "attributes" section.
131 changes: 109 additions & 22 deletions tests/test_parsers/test_docstrings/test_google.py
Original file line number Diff line number Diff line change
@@ -52,6 +52,9 @@ def test_sections_without_signature():
nada: SEGFAULT.
rien: SEGFAULT.
Keyword Args:
keywd: SEGFAULT.
Exceptions:
GlobalError: when nothing works as expected.
@@ -60,8 +63,8 @@ def test_sections_without_signature():
"""
)

assert len(sections) == 4
assert len(errors) == 5 # missing annotations for params and return
assert len(sections) == 5
assert len(errors) == 6 # missing annotations for params and return
for error in errors[:-1]:
assert "param" in error
assert "return" in errors[-1]
@@ -79,43 +82,49 @@ def test_property_docstring():
def test_function_without_annotations():
"""Parse a function docstring without signature annotations."""

def f(x, y):
def f(x, y, *, z):
"""
This function has no annotations.
Parameters:
x: X value.
y: Y value.
Keyword Args:
z: Z value.
Returns:
Sum X + Y.
Sum X + Y + Z.
"""
return x + y
return x + y + z

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 3
assert len(sections) == 4
assert len(errors) == 1
assert "No type in return" in errors[0]


def test_function_with_annotations():
"""Parse a function docstring with signature annotations."""

def f(x: int, y: int) -> int:
def f(x: int, y: int, *, z: int) -> int:
"""
This function has annotations.
Parameters:
x: X value.
y: Y value.
Keyword Arguments:
z: Z value.
Returns:
Sum X + Y.
"""
return x + y

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


@@ -188,25 +197,34 @@ def f(x: int, y: int) -> int:
def test_types_in_docstring():
"""Parse types in docstring."""

def f(x, y):
def f(x, y, *, z):
"""
The types are written in the docstring.
Parameters:
x (int): X value.
y (int): Y value.
Keyword Args:
z (int): Z value.
Returns:
int: Sum X + Y.
int: Sum X + Y + Z.
"""
return x + y
return x + y + z

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 3
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
r = sections[2].value
(z,) = sections[2].value
r = sections[3].value

assert x.name == "x"
assert x.annotation == "int"
@@ -220,31 +238,45 @@ def f(x, y):
assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
assert y.default is inspect.Signature.empty

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

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


def test_types_and_optional_in_docstring():
"""Parse optional types in docstring."""

def f(x=1, y=None):
def f(x=1, y=None, *, z=None):
"""
The types are written in the docstring.
Parameters:
x (int): X value.
y (int, optional): Y value.
Keyword Args:
z (int, optional): Z value.
Returns:
int: Sum X + Y.
int: Sum X + Y + Z.
"""
return x + (y or 1)
return x + (y or 1) + (z or 1)

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 3
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

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

assert x.name == "x"
assert x.annotation == "int"
@@ -258,25 +290,34 @@ def f(x=1, y=None):
assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
assert y.default is None

assert z.name == "z"
assert z.annotation == "int"
assert z.description == "Z value."
assert z.kind is inspect.Parameter.KEYWORD_ONLY
assert z.default is None


def test_types_in_signature_and_docstring():
"""Parse types in both signature and docstring."""

def f(x: int, y: int) -> int:
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.
Keyword Args:
z (int): Z value.
Returns:
int: Sum X + Y.
int: Sum X + Y + Z.
"""
return x + y
return x + y + z

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


@@ -401,6 +442,23 @@ def f(x: int):
assert "Empty" in errors[1]


def test_param_line_without_colon_keyword_only():
"""Warn when missing colon."""

def f(*, x: int):
"""
Keyword Args:
x is an integer.
"""
return x

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert not sections # getting x fails, so the section is empty and discarded
assert len(errors) == 2
assert "pair" in errors[0]
assert "Empty" in errors[1]


def test_admonitions():
"""Parse admonitions."""

@@ -493,6 +551,35 @@ def f(a, *args, **kwargs):
assert not errors


def test_parse_args_kwargs_keyword_only():
"""Parse args and kwargs."""

def f(a, *args, **kwargs):
"""
Arguments:
a: a parameter.
*args: args parameters.
Keyword Args:
**kwargs: kwargs parameters.
"""
return 1

sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
assert len(sections) == 2
expected_parameters = {"a": "a parameter.", "*args": "args parameters."}
for param in sections[0].value:
assert param.name in expected_parameters
assert expected_parameters[param.name] == param.description

expected_parameters = {"**kwargs": "kwargs parameters."}
for param in sections[1].value:
assert param.name in expected_parameters
assert expected_parameters[param.name] == param.description

assert not errors


def test_different_indentation():
"""Parse different indentations, warn on confusing indentation."""

0 comments on commit 0133369

Please sign in to comment.