Skip to content

Commit 0133369

Browse files
authored
feat: Support "Keyword Args" sections for Gooogle-style
Add support for "Keyword Args" and "Keyword Arguments" sections for Google-style docstrings. Issue #88: #88 PR #105: #105
1 parent 8e1b1b2 commit 0133369

File tree

3 files changed

+149
-24
lines changed

3 files changed

+149
-24
lines changed

Diff for: src/pytkdocs/parsers/docstrings/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class Type:
107107
RETURN = "return"
108108
EXAMPLES = "examples"
109109
ATTRIBUTES = "attributes"
110+
KEYWORD_ARGS = "keyword_args"
110111

111112
def __init__(self, section_type: str, value: Any) -> None:
112113
"""

Diff for: src/pytkdocs/parsers/docstrings/google.py

+39-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"arguments:": Section.Type.PARAMETERS,
1010
"params:": Section.Type.PARAMETERS,
1111
"parameters:": Section.Type.PARAMETERS,
12+
"keyword args:": Section.Type.KEYWORD_ARGS,
13+
"keyword arguments:": Section.Type.KEYWORD_ARGS,
1214
"raise:": Section.Type.EXCEPTIONS,
1315
"raises:": Section.Type.EXCEPTIONS,
1416
"except:": Section.Type.EXCEPTIONS,
@@ -39,6 +41,7 @@ def __init__(self, replace_admonitions: bool = True) -> None:
3941
self.replace_admonitions = replace_admonitions
4042
self.section_reader = {
4143
Section.Type.PARAMETERS: self.read_parameters_section,
44+
Section.Type.KEYWORD_ARGS: self.read_keyword_arguments_section,
4245
Section.Type.EXCEPTIONS: self.read_exceptions_section,
4346
Section.Type.EXAMPLES: self.read_examples_section,
4447
Section.Type.ATTRIBUTES: self.read_attributes_section,
@@ -213,9 +216,9 @@ def read_block(self, lines: List[str], start_index: int) -> Tuple[str, int]:
213216

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

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

271+
return parameters, i
272+
273+
def read_parameters_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
274+
"""
275+
Parse a "parameters" section.
276+
277+
Arguments:
278+
lines: The parameters block lines.
279+
start_index: The line number to start at.
280+
281+
Returns:
282+
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
283+
"""
284+
parameters, i = self._parse_parameters_section(lines, start_index)
285+
268286
if parameters:
269287
return Section(Section.Type.PARAMETERS, parameters), i
270288

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

292+
def read_keyword_arguments_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
293+
"""
294+
Parse a "keyword arguments" section.
295+
296+
Arguments:
297+
lines: The parameters block lines.
298+
start_index: The line number to start at.
299+
300+
Returns:
301+
A tuple containing a `Section` (or `None`) and the index at which to continue parsing.
302+
"""
303+
parameters, i = self._parse_parameters_section(lines, start_index)
304+
305+
if parameters:
306+
return Section(Section.Type.KEYWORD_ARGS, parameters), i
307+
308+
self.error(f"Empty keyword arguments section at line {start_index}")
309+
return None, i
310+
274311
def read_attributes_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
275312
"""
276313
Parse an "attributes" section.

Diff for: tests/test_parsers/test_docstrings/test_google.py

+109-22
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def test_sections_without_signature():
5252
nada: SEGFAULT.
5353
rien: SEGFAULT.
5454
55+
Keyword Args:
56+
keywd: SEGFAULT.
57+
5558
Exceptions:
5659
GlobalError: when nothing works as expected.
5760
@@ -60,8 +63,8 @@ def test_sections_without_signature():
6063
"""
6164
)
6265

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

82-
def f(x, y):
85+
def f(x, y, *, z):
8386
"""
8487
This function has no annotations.
8588
8689
Parameters:
8790
x: X value.
8891
y: Y value.
8992
93+
Keyword Args:
94+
z: Z value.
95+
9096
Returns:
91-
Sum X + Y.
97+
Sum X + Y + Z.
9298
"""
93-
return x + y
99+
return x + y + z
94100

95101
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
96-
assert len(sections) == 3
102+
assert len(sections) == 4
97103
assert len(errors) == 1
98104
assert "No type in return" in errors[0]
99105

100106

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

104-
def f(x: int, y: int) -> int:
110+
def f(x: int, y: int, *, z: int) -> int:
105111
"""
106112
This function has annotations.
107113
108114
Parameters:
109115
x: X value.
110116
y: Y value.
111117
118+
Keyword Arguments:
119+
z: Z value.
120+
112121
Returns:
113122
Sum X + Y.
114123
"""
115124
return x + y
116125

117126
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
118-
assert len(sections) == 3
127+
assert len(sections) == 4
119128
assert not errors
120129

121130

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

191-
def f(x, y):
200+
def f(x, y, *, z):
192201
"""
193202
The types are written in the docstring.
194203
195204
Parameters:
196205
x (int): X value.
197206
y (int): Y value.
198207
208+
Keyword Args:
209+
z (int): Z value.
210+
199211
Returns:
200-
int: Sum X + Y.
212+
int: Sum X + Y + Z.
201213
"""
202-
return x + y
214+
return x + y + z
203215

204216
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
205-
assert len(sections) == 3
217+
assert len(sections) == 4
206218
assert not errors
207219

220+
assert sections[0].type == Section.Type.MARKDOWN
221+
assert sections[1].type == Section.Type.PARAMETERS
222+
assert sections[2].type == Section.Type.KEYWORD_ARGS
223+
assert sections[3].type == Section.Type.RETURN
224+
208225
x, y = sections[1].value
209-
r = sections[2].value
226+
(z,) = sections[2].value
227+
r = sections[3].value
210228

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

241+
assert z.name == "z"
242+
assert z.annotation == "int"
243+
assert z.description == "Z value."
244+
assert z.kind is inspect.Parameter.KEYWORD_ONLY
245+
assert z.default is inspect.Signature.empty
246+
223247
assert r.annotation == "int"
224-
assert r.description == "Sum X + Y."
248+
assert r.description == "Sum X + Y + Z."
225249

226250

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

230-
def f(x=1, y=None):
254+
def f(x=1, y=None, *, z=None):
231255
"""
232256
The types are written in the docstring.
233257
234258
Parameters:
235259
x (int): X value.
236260
y (int, optional): Y value.
237261
262+
Keyword Args:
263+
z (int, optional): Z value.
264+
238265
Returns:
239-
int: Sum X + Y.
266+
int: Sum X + Y + Z.
240267
"""
241-
return x + (y or 1)
268+
return x + (y or 1) + (z or 1)
242269

243270
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
244-
assert len(sections) == 3
271+
assert len(sections) == 4
245272
assert not errors
246273

274+
assert sections[0].type == Section.Type.MARKDOWN
275+
assert sections[1].type == Section.Type.PARAMETERS
276+
assert sections[2].type == Section.Type.KEYWORD_ARGS
277+
247278
x, y = sections[1].value
279+
(z,) = sections[2].value
248280

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

293+
assert z.name == "z"
294+
assert z.annotation == "int"
295+
assert z.description == "Z value."
296+
assert z.kind is inspect.Parameter.KEYWORD_ONLY
297+
assert z.default is None
298+
261299

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

265-
def f(x: int, y: int) -> int:
303+
def f(x: int, y: int, *, z: int) -> int:
266304
"""
267305
The types are written both in the signature and in the docstring.
268306
269307
Parameters:
270308
x (int): X value.
271309
y (int): Y value.
272310
311+
Keyword Args:
312+
z (int): Z value.
313+
273314
Returns:
274-
int: Sum X + Y.
315+
int: Sum X + Y + Z.
275316
"""
276-
return x + y
317+
return x + y + z
277318

278319
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
279-
assert len(sections) == 3
320+
assert len(sections) == 4
280321
assert not errors
281322

282323

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

403444

445+
def test_param_line_without_colon_keyword_only():
446+
"""Warn when missing colon."""
447+
448+
def f(*, x: int):
449+
"""
450+
Keyword Args:
451+
x is an integer.
452+
"""
453+
return x
454+
455+
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
456+
assert not sections # getting x fails, so the section is empty and discarded
457+
assert len(errors) == 2
458+
assert "pair" in errors[0]
459+
assert "Empty" in errors[1]
460+
461+
404462
def test_admonitions():
405463
"""Parse admonitions."""
406464

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

495553

554+
def test_parse_args_kwargs_keyword_only():
555+
"""Parse args and kwargs."""
556+
557+
def f(a, *args, **kwargs):
558+
"""
559+
Arguments:
560+
a: a parameter.
561+
*args: args parameters.
562+
563+
Keyword Args:
564+
**kwargs: kwargs parameters.
565+
"""
566+
return 1
567+
568+
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
569+
assert len(sections) == 2
570+
expected_parameters = {"a": "a parameter.", "*args": "args parameters."}
571+
for param in sections[0].value:
572+
assert param.name in expected_parameters
573+
assert expected_parameters[param.name] == param.description
574+
575+
expected_parameters = {"**kwargs": "kwargs parameters."}
576+
for param in sections[1].value:
577+
assert param.name in expected_parameters
578+
assert expected_parameters[param.name] == param.description
579+
580+
assert not errors
581+
582+
496583
def test_different_indentation():
497584
"""Parse different indentations, warn on confusing indentation."""
498585

0 commit comments

Comments
 (0)