diff --git a/CHANGES b/CHANGES index 95a2e1906ee..18fb2ce6ed7 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Incompatible changes :confval:`autosummary_generate_overwrite` to change the behavior * #5923: autodoc: the members of ``object`` class are not documented by default when ``:inherited-members:`` and ``:special-members:`` are given. +* #6417: py domain: doctree of desc_parameterlist has been changed. The + argument names, annotations and default values are wrapped with inline node Deprecated ---------- @@ -31,6 +33,7 @@ Features added not to document inherited members of the class and uppers * #6558: glossary: emit a warning for duplicated glossary entry * #6558: std domain: emit a warning for duplicated generic objects +* #6417: py domain: Allow to make a style for arguments of functions and methods Bugs fixed ---------- diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index f23c5025614..6603ebefd65 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -30,6 +30,7 @@ from sphinx.util import logging from sphinx.util.docfields import Field, GroupedField, TypedField from sphinx.util.docutils import SphinxDirective +from sphinx.util.inspect import signature_from_str from sphinx.util.nodes import make_refnode from sphinx.util.typing import TextlikeNode @@ -62,6 +63,34 @@ } +def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: + """Parse a list of arguments using python parser""" + params = addnodes.desc_parameterlist(arglist) + sig = signature_from_str('(%s)' % arglist) + for param in sig.parameters.values(): + node = addnodes.desc_parameter() + if param.kind == param.VAR_POSITIONAL: + node += nodes.inline('', '*' + param.name, classes=['argument']) + elif param.kind == param.VAR_KEYWORD: + node += nodes.inline('', '**' + param.name, classes=['argument']) + else: + node += nodes.inline('', param.name, classes=['argument']) + + if param.annotation is not param.empty: + node += nodes.Text(': ') + node += nodes.inline('', param.annotation, classes=['annotation']) + if param.default is not param.empty: + if param.annotation is not param.empty: + node += nodes.Text(' = ') + else: + node += nodes.Text('=') + node += nodes.inline('', param.default, classes=['default_value']) + + params += node + + return params + + def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None: """"Parse" a list of arguments separated by commas. @@ -284,7 +313,15 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str] signode += addnodes.desc_name(name, name) if arglist: - _pseudo_parse_arglist(signode, arglist) + try: + signode += _parse_arglist(arglist) + except SyntaxError: + # fallback to parse arglist original parser. + # it supports to represent optional arguments (ex. "func(foo [, bar])") + _pseudo_parse_arglist(signode, arglist) + except NotImplementedError as exc: + logger.warning(exc) + _pseudo_parse_arglist(signode, arglist) else: if self.needs_arglist(): # for callables, add an empty parameter list diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 63a5948af6f..ef1a36fc853 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -177,7 +177,7 @@ def test_html4_output(app, status, warning): ], 'autodoc.html': [ (".//dt[@id='autodoc_target.Class']", ''), - (".//dt[@id='autodoc_target.function']/em", r'\*\*kwds'), + (".//dt[@id='autodoc_target.function']/em/span[@class='argument']", r'\*\*kwds'), (".//dd/p", r'Return spam\.'), ], 'extapi.html': [ diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 3ff29cbb7a6..e67719a130d 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -223,7 +223,43 @@ def test_pyfunction_signature(app): desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, "name: str"]) + assert_node(doctree[1][0][1], + [desc_parameterlist, desc_parameter, ([nodes.inline, "name"], + ": ", + [nodes.inline, "str"])]) + + +def test_pyfunction_signature_full(app): + text = (".. py:function:: hello(a: str, b = 1, *args: str, " + "c: bool = True, **kwargs: str) -> str") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, "str"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", noindex=False) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([nodes.inline, "a"], + ": ", + [nodes.inline, "str"])], + [desc_parameter, ([nodes.inline, "b"], + "=", + [nodes.inline, "1"])], + [desc_parameter, ([nodes.inline, "*args"], + ": ", + [nodes.inline, "str"])], + [desc_parameter, ([nodes.inline, "c"], + ": ", + [nodes.inline, "bool"], + " = ", + [nodes.inline, "True"])], + [desc_parameter, ([nodes.inline, "**kwargs"], + ": ", + [nodes.inline, "str"])])]) + + def test_optional_pyfunction_signature(app):