Skip to content

Commit 70dda21

Browse files
authored
feat: Parse Sphinx parameter types as expressions
PR-392: #392
1 parent acafbd8 commit 70dda21

File tree

2 files changed

+118
-9
lines changed

2 files changed

+118
-9
lines changed

src/_griffe/docstrings/sphinx.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
DocstringSectionReturns,
2323
DocstringSectionText,
2424
)
25-
from _griffe.docstrings.utils import docstring_warning
25+
from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation
2626

2727
if TYPE_CHECKING:
2828
from _griffe.expressions import Expr
@@ -75,7 +75,7 @@ class _ParsedValues:
7575

7676
description: list[str] = field(default_factory=list)
7777
parameters: dict[str, DocstringParameter] = field(default_factory=dict)
78-
param_types: dict[str, str] = field(default_factory=dict)
78+
param_types: dict[str, str | Expr] = field(default_factory=dict)
7979
attributes: dict[str, DocstringAttribute] = field(default_factory=dict)
8080
attribute_types: dict[str, str] = field(default_factory=dict)
8181
exceptions: list[DocstringRaise] = field(default_factory=list)
@@ -145,7 +145,10 @@ def _read_parameter(
145145
# no type info
146146
name = parsed_directive.directive_parts[1]
147147
elif len(parsed_directive.directive_parts) == 3: # noqa: PLR2004
148-
directive_type = parsed_directive.directive_parts[1]
148+
directive_type = parse_docstring_annotation(
149+
parsed_directive.directive_parts[1],
150+
docstring,
151+
)
149152
name = parsed_directive.directive_parts[2]
150153
else:
151154
if warnings:
@@ -191,7 +194,7 @@ def _determine_param_default(docstring: Docstring, name: str) -> str | None:
191194
def _determine_param_annotation(
192195
docstring: Docstring,
193196
name: str,
194-
directive_type: str | None,
197+
directive_type: str | Expr | None,
195198
parsed_values: _ParsedValues,
196199
*,
197200
warnings: bool = True,
@@ -234,7 +237,8 @@ def _read_parameter_type(
234237
parsed_directive = _parse_directive(docstring, offset, warnings=warnings)
235238
if parsed_directive.invalid:
236239
return parsed_directive.next_index
237-
param_type = _consolidate_descriptive_type(parsed_directive.value.strip())
240+
param_type_str = _consolidate_descriptive_type(parsed_directive.value.strip())
241+
param_type = parse_docstring_annotation(param_type_str, docstring)
238242

239243
if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004
240244
param_name = parsed_directive.directive_parts[1]

tests/test_docstrings/test_sphinx.py

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
DocstringRaise,
1616
DocstringReturn,
1717
DocstringSectionKind,
18+
Expr,
19+
ExprBinOp,
20+
ExprName,
1821
Function,
1922
Module,
2023
Parameter,
@@ -231,28 +234,62 @@ def test_parse__param_field_docs_type__param_section_with_type(parse_sphinx: Par
231234
assert actual.as_dict() == expected.as_dict()
232235

233236

234-
def test_parse__param_field_type_field__param_section_with_type(parse_sphinx: ParserType) -> None:
237+
@pytest.mark.parametrize("type_", ["str", "int"])
238+
def test_parse__param_field_type_field__param_section_with_type(parse_sphinx: ParserType, type_: str) -> None:
235239
"""Parse parameters with separated types.
236240
237241
Parameters:
238242
parse_sphinx: Fixture parser.
243+
type_: The type to use in the type directive.
239244
"""
240245
docstring = f"""
241246
Docstring with line continuation.
242247
243-
:param foo: {SOME_TEXT}
244-
:type foo: str
248+
:param {SOME_NAME}: {SOME_TEXT}
249+
:type {SOME_NAME}: {type_}
245250
"""
246251

247252
sections, _ = parse_sphinx(docstring)
248253
assert len(sections) == 2
249254
assert sections[1].kind is DocstringSectionKind.parameters
250255
actual = sections[1].value[0]
251-
expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT)
256+
expected = DocstringParameter(SOME_NAME, annotation=f"{type_}", description=SOME_TEXT)
252257
assert isinstance(actual, type(expected))
253258
assert actual.as_dict() == expected.as_dict()
254259

255260

261+
@pytest.mark.parametrize("type_", ["str", "int"])
262+
def test_parse__param_field_type_field__param_section_with_type_with_parent(
263+
parse_sphinx: ParserType,
264+
type_: str,
265+
) -> None:
266+
"""Parse parameters with separated types.
267+
268+
Parameters:
269+
parse_sphinx: Fixture parser.
270+
type_: The type to use in the type directive.
271+
"""
272+
docstring = f"""
273+
Docstring with line continuation.
274+
275+
:param {SOME_NAME}: {SOME_TEXT}
276+
:type {SOME_NAME}: {type_}
277+
"""
278+
parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME)))
279+
sections, _ = parse_sphinx(docstring, parent=parent_fn)
280+
assert len(sections) == 2
281+
assert sections[1].kind is DocstringSectionKind.parameters
282+
actual = sections[1].value[0]
283+
expected_annotation = ExprName(name=f"{type_}")
284+
expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT)
285+
assert isinstance(actual, type(expected))
286+
assert actual.as_dict() == expected.as_dict()
287+
assert isinstance(actual.annotation, type(expected.annotation))
288+
assert isinstance(actual.annotation, ExprName)
289+
assert isinstance(actual.annotation, Expr)
290+
assert actual.annotation.as_dict() == expected_annotation.as_dict()
291+
292+
256293
def test_parse__param_field_type_field_first__param_section_with_type(parse_sphinx: ParserType) -> None:
257294
"""Parse parameters with separated types first.
258295
@@ -275,6 +312,33 @@ def test_parse__param_field_type_field_first__param_section_with_type(parse_sphi
275312
assert actual.as_dict() == expected.as_dict()
276313

277314

315+
def test_parse__param_field_type_field_first__param_section_with_type_with_parent(parse_sphinx: ParserType) -> None:
316+
"""Parse parameters with separated types first.
317+
318+
Parameters:
319+
parse_sphinx: Fixture parser.
320+
"""
321+
docstring = f"""
322+
Docstring with line continuation.
323+
324+
:type {SOME_NAME}: str
325+
:param {SOME_NAME}: {SOME_TEXT}
326+
"""
327+
parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME)))
328+
sections, _ = parse_sphinx(docstring, parent=parent_fn)
329+
assert len(sections) == 2
330+
assert sections[1].kind is DocstringSectionKind.parameters
331+
actual = sections[1].value[0]
332+
expected_annotation = ExprName("str", parent=Class("C"))
333+
expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT)
334+
assert isinstance(actual, type(expected))
335+
assert actual.as_dict() == expected.as_dict()
336+
assert isinstance(actual.annotation, type(expected.annotation))
337+
assert isinstance(actual.annotation, ExprName)
338+
assert isinstance(actual.annotation, Expr)
339+
assert actual.annotation.as_dict() == expected_annotation.as_dict()
340+
341+
278342
@pytest.mark.parametrize("union", ["str or None", "None or str", "str or int", "str or int or float"])
279343
def test_parse__param_field_type_field_or_none__param_section_with_optional(
280344
parse_sphinx: ParserType,
@@ -302,6 +366,47 @@ def test_parse__param_field_type_field_or_none__param_section_with_optional(
302366
assert actual.as_dict() == expected.as_dict()
303367

304368

369+
@pytest.mark.parametrize(
370+
("union", "expected_annotation"),
371+
[
372+
("str or None", ExprBinOp(ExprName("str"), "|", "None")),
373+
("None or str", ExprBinOp("None", "|", ExprName("str"))),
374+
("str or int", ExprBinOp(ExprName("str"), "|", ExprName("int"))),
375+
("str or int or float", ExprBinOp(ExprBinOp(ExprName("str"), "|", ExprName("int")), "|", ExprName("float"))),
376+
],
377+
)
378+
def test_parse__param_field_type_field_or_none__param_section_with_optional_with_parent(
379+
parse_sphinx: ParserType,
380+
union: str,
381+
expected_annotation: Expr,
382+
) -> None:
383+
"""Parse parameters with separated union types.
384+
385+
Parameters:
386+
parse_sphinx: Fixture parser.
387+
union: A parametrized union type.
388+
expected_annotation: The expected annotation as an expression
389+
"""
390+
docstring = f"""
391+
Docstring with line continuation.
392+
393+
:param {SOME_NAME}: {SOME_TEXT}
394+
:type {SOME_NAME}: {union}
395+
"""
396+
397+
parent_fn = Function("func", parameters=Parameters(Parameter(SOME_NAME)))
398+
sections, _ = parse_sphinx(docstring, parent=parent_fn)
399+
assert len(sections) == 2
400+
assert sections[1].kind is DocstringSectionKind.parameters
401+
actual = sections[1].value[0]
402+
expected = DocstringParameter(SOME_NAME, annotation=expected_annotation, description=SOME_TEXT)
403+
assert isinstance(actual, type(expected))
404+
assert actual.as_dict() == expected.as_dict()
405+
assert isinstance(actual.annotation, type(expected.annotation))
406+
assert isinstance(actual.annotation, Expr)
407+
assert actual.annotation.as_dict() == expected_annotation.as_dict()
408+
409+
305410
def test_parse__param_field_annotate_type__param_section_with_type(parse_sphinx: ParserType) -> None:
306411
"""Parse a simple docstring.
307412

0 commit comments

Comments
 (0)