Skip to content

Commit c4f6bdc

Browse files
authored
refactor: Switch preference order between annotation and docstring type
With this change, types written in docstrings are used in priority. If no type is written in the docstring for a parameter, the annotation is used instead. This allows to override what is ultimately shown by mkdocstrings.
1 parent 88b457f commit c4f6bdc

File tree

3 files changed

+72
-38
lines changed

3 files changed

+72
-38
lines changed

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

+32-27
Original file line numberDiff line numberDiff line change
@@ -232,36 +232,41 @@ def _parse_parameters_section(self, lines: List[str], start_index: int) -> Tuple
232232
block, i = self.read_block_items(lines, start_index)
233233

234234
for param_line in block:
235+
236+
# Check that there is an annotation in the docstring
235237
try:
236238
name_with_type, description = param_line.split(":", 1)
237239
except ValueError:
238240
self.error(f"Failed to get 'name: description' pair from '{param_line}'")
239241
continue
240242

243+
# Setting defaults
244+
default = empty
245+
annotation = empty
246+
kind = None
247+
# Can only get description from docstring - keep if no type was given
241248
description = description.lstrip()
242249

250+
# If we have managed to find a type in the docstring use this
243251
if " " in name_with_type:
244252
name, type_ = name_with_type.split(" ", 1)
245-
type_ = type_.strip("()")
246-
if type_.endswith(", optional"):
247-
type_ = type_[:-10]
253+
annotation = type_.strip("()")
254+
if annotation.endswith(", optional"): # type: ignore
255+
annotation = annotation[:-10] # type: ignore
256+
# Otherwise try to use the signature as `annotation` would still be empty
248257
else:
249258
name = name_with_type
250-
type_ = empty
251-
252-
default = empty
253-
annotation = type_
254-
kind = None
255259

260+
# Check in the signature to get extra details
256261
try:
257262
signature_param = self.context["signature"].parameters[name.lstrip("*")] # type: ignore
258263
except (AttributeError, KeyError):
259264
self.error(f"No type annotation for parameter '{name}'")
260265
else:
261-
if signature_param.annotation is not empty:
266+
# If signature_param.X are empty it doesnt matter as defaults are empty anyway
267+
if annotation is empty:
262268
annotation = signature_param.annotation
263-
if signature_param.default is not empty:
264-
default = signature_param.default
269+
default = signature_param.default
265270
kind = signature_param.kind
266271

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

392-
if self.context["signature"]:
393-
annotation = self.context["signature"].return_annotation
394-
else:
395-
annotation = self.context["annotation"]
396-
397-
if annotation is empty:
398-
if text:
399-
try:
400-
type_, text = text.split(":", 1)
401-
except ValueError:
402-
self.error("No type in return description")
403-
else:
404-
annotation = type_.lstrip()
405-
text = text.lstrip()
399+
# First try to get the annotation and description from the docstring
400+
if text:
401+
try:
402+
type_, text = text.split(":", 1)
403+
except ValueError:
404+
self.error("No type in return description")
406405
else:
407-
self.error("No return type annotation")
406+
annotation = type_.lstrip()
407+
description = text.lstrip()
408+
409+
# If there was no annotation in the docstring then move to signature
410+
if annotation is empty and self.context["signature"]:
411+
annotation = self.context["signature"].return_annotation
408412

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

413-
return Section(Section.Type.RETURN, AnnotatedObject(annotation, text)), i
418+
return Section(Section.Type.RETURN, AnnotatedObject(annotation, description)), i
414419

415420
def read_examples_section(self, lines: List[str], start_index: int) -> Tuple[Optional[Section], int]:
416421
"""

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

+38-8
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_property_docstring():
7676
prop = class_.attributes[0]
7777
sections, errors = prop.docstring_sections, prop.docstring_errors
7878
assert len(sections) == 2
79-
assert not errors
79+
assert len(errors) == 1
8080

8181

8282
def test_function_without_annotations():
@@ -125,7 +125,7 @@ def f(x: int, y: int, *, z: int) -> int:
125125

126126
sections, errors = parse(inspect.getdoc(f), inspect.signature(f))
127127
assert len(sections) == 4
128-
assert not errors
128+
assert len(errors) == 1
129129

130130

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

299299

300300
def test_types_in_signature_and_docstring():
301-
"""Parse types in both signature and docstring."""
301+
"""Parse types in both signature and docstring. Should prefer the docstring type"""
302302

303303
def f(x: int, y: int, *, z: int) -> int:
304304
"""
305305
The types are written both in the signature and in the docstring.
306306
307307
Parameters:
308-
x (int): X value.
309-
y (int): Y value.
308+
x (str): X value.
309+
y (str): Y value.
310310
311311
Keyword Args:
312-
z (int): Z value.
312+
z (str): Z value.
313313
314314
Returns:
315-
int: Sum X + Y + Z.
315+
str: Sum X + Y + Z.
316316
"""
317317
return x + y + z
318318

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

323+
assert sections[0].type == Section.Type.MARKDOWN
324+
assert sections[1].type == Section.Type.PARAMETERS
325+
assert sections[2].type == Section.Type.KEYWORD_ARGS
326+
assert sections[3].type == Section.Type.RETURN
327+
328+
x, y = sections[1].value
329+
(z,) = sections[2].value
330+
r = sections[3].value
331+
332+
assert x.name == "x"
333+
assert x.annotation == "str"
334+
assert x.description == "X value."
335+
assert x.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
336+
assert x.default is inspect.Signature.empty
337+
338+
assert y.name == "y"
339+
assert y.annotation == "str"
340+
assert y.description == "Y value."
341+
assert y.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
342+
assert y.default is inspect.Signature.empty
343+
344+
assert z.name == "z"
345+
assert z.annotation == "str"
346+
assert z.description == "Z value."
347+
assert z.kind is inspect.Parameter.KEYWORD_ONLY
348+
assert z.default is inspect.Signature.empty
349+
350+
assert r.annotation == "str"
351+
assert r.description == "Sum X + Y + Z."
352+
323353

324354
def test_close_sections():
325355
"""Parse sections without blank lines in between."""
@@ -498,7 +528,7 @@ def f():
498528
assert len(sections) == 1
499529
for error in errors[:3]:
500530
assert "Empty" in error
501-
assert "No return type" in errors[3]
531+
assert "Empty return section at line" in errors[3]
502532
assert "Empty" in errors[-1]
503533

504534

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ class DummyObject:
1414
def parse(docstring, signature=None, return_type=inspect.Signature.empty):
1515
"""Helper to parse a doctring."""
1616
return Numpy().parse(
17-
dedent(docstring).strip(),
18-
{"obj": DummyObject(), "signature": signature, "type": return_type},
17+
dedent(docstring).strip(), {"obj": DummyObject(), "signature": signature, "type": return_type},
1918
)
2019

2120

@@ -80,7 +79,7 @@ def test_property_docstring():
8079
prop = class_.attributes[0]
8180
sections, errors = prop.docstring_sections, prop.docstring_errors
8281
assert len(sections) == 2
83-
assert not errors
82+
assert len(errors) == 1
8483

8584

8685
def test_function_without_annotations():

0 commit comments

Comments
 (0)