Skip to content

Allow configuration of trailing commas in multi-line signatures #12975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Features added
* #7630, #4824: autodoc: Use :file:`.pyi` type stub files
to auto-document native modules.
Patch by Adam Turner, partially based on work by Allie Fitter.
* #12975: Enable configuration of trailing commas in multi-line signatures
in the Python and Javascript domains, via the new
:confval:`python_trailing_comma_in_multi_line_signatures` and
:confval:`javascript_trailing_comma_in_multi_line_signatures`
configuration options.

Bugs fixed
----------
Expand Down Expand Up @@ -86,6 +91,7 @@ Bugs fixed
before static methods, which themselves are rendered before regular
methods and attributes.
Patch by Bénédikt Tran.
* #12975: Avoid rendering a trailing comma in C and C++ multi-line signatures.

Testing
-------
Expand Down
16 changes: 16 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4092,6 +4092,14 @@ Options for the Javascript domain

.. versionadded:: 7.1

.. confval:: javascript_trailing_comma_in_multi_line_signatures
:type: :code-py:`bool`
:default: :code-py:`True`

Use a trailing comma in parameter lists spanning multiple lines, if true.

.. versionadded:: 8.2


Options for the Python domain
-----------------------------
Expand Down Expand Up @@ -4181,6 +4189,14 @@ Options for the Python domain

.. versionadded:: 7.1

.. confval:: python_trailing_comma_in_multi_line_signatures
:type: :code-py:`bool`
:default: :code-py:`True`

Use a trailing comma in parameter lists spanning multiple lines, if true.

.. versionadded:: 8.2

.. confval:: python_use_unqualified_type_names
:type: :code-py:`bool`
:default: :code-py:`False`
Expand Down
4 changes: 4 additions & 0 deletions sphinx/addnodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ class desc_parameterlist(nodes.Part, nodes.Inline, nodes.FixedTextElement):
As default the parameter list is written in line with the rest of the signature.
Set ``multi_line_parameter_list = True`` to describe a multi-line parameter list.
In that case each parameter will then be written on its own, indented line.
A trailing comma will be added on the last line
if ``multi_line_trailing_comma`` is True.
"""

child_text_separator = ', '
Expand All @@ -273,6 +275,8 @@ class desc_type_parameter_list(nodes.Part, nodes.Inline, nodes.FixedTextElement)
As default the type parameters list is written in line with the rest of the signature.
Set ``multi_line_parameter_list = True`` to describe a multi-line type parameters list.
In that case each type parameter will then be written on its own, indented line.
A trailing comma will be added on the last line
if ``multi_line_trailing_comma`` is True.
"""

child_text_separator = ', '
Expand Down
17 changes: 16 additions & 1 deletion sphinx/domains/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
and (len(sig) > max_len > 0)
)

trailing_comma = (
self.env.config.javascript_trailing_comma_in_multi_line_signatures
)

display_prefix = self.get_display_prefix()
if display_prefix:
signode += addnodes.desc_annotation('', '', *display_prefix)
Expand All @@ -129,7 +133,12 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
if not arglist:
signode += addnodes.desc_parameterlist()
else:
_pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
_pseudo_parse_arglist(
signode,
arglist,
multi_line_parameter_list,
trailing_comma,
)
return fullname, prefix

def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]:
Expand Down Expand Up @@ -564,6 +573,12 @@ def setup(app: Sphinx) -> ExtensionMetadata:
'env',
types=frozenset({int, type(None)}),
)
app.add_config_value(
'javascript_trailing_comma_in_multi_line_signatures',
True,
'env',
types=frozenset({bool}),
)
return {
'version': 'builtin',
'env_version': 3,
Expand Down
6 changes: 6 additions & 0 deletions sphinx/domains/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,12 @@ def setup(app: Sphinx) -> ExtensionMetadata:
'env',
types=frozenset({int, type(None)}),
)
app.add_config_value(
'python_trailing_comma_in_multi_line_signatures',
True,
'env',
types=frozenset({bool}),
)
app.add_config_value('python_display_short_literal_types', False, 'env')
app.connect('object-description-transform', filter_meta_fields)
app.connect('missing-reference', builtin_resolver, priority=900)
Expand Down
18 changes: 15 additions & 3 deletions sphinx/domains/python/_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,15 @@ def _pformat_token(self, tok: Token, native: bool = False) -> str:


def _parse_type_list(
tp_list: str, env: BuildEnvironment, multi_line_parameter_list: bool = False
tp_list: str,
env: BuildEnvironment,
multi_line_parameter_list: bool = False,
trailing_comma: bool = True,
) -> addnodes.desc_type_parameter_list:
"""Parse a list of type parameters according to PEP 695."""
type_params = addnodes.desc_type_parameter_list(tp_list)
type_params['multi_line_parameter_list'] = multi_line_parameter_list
type_params['multi_line_trailing_comma'] = trailing_comma
# formal parameter names are interpreted as type parameter names and
# type annotations are interpreted as type parameter bound or constraints
parser = _TypeParameterListParser(tp_list)
Expand Down Expand Up @@ -462,11 +466,15 @@ def _parse_type_list(


def _parse_arglist(
arglist: str, env: BuildEnvironment, multi_line_parameter_list: bool = False
arglist: str,
env: BuildEnvironment,
multi_line_parameter_list: bool = False,
trailing_comma: bool = True,
) -> addnodes.desc_parameterlist:
"""Parse a list of arguments using AST parser"""
params = addnodes.desc_parameterlist(arglist)
params['multi_line_parameter_list'] = multi_line_parameter_list
params['multi_line_trailing_comma'] = trailing_comma
sig = signature_from_str('(%s)' % arglist)
last_kind = None
for param in sig.parameters.values():
Expand Down Expand Up @@ -522,7 +530,10 @@ def _parse_arglist(


def _pseudo_parse_arglist(
signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False
signode: desc_signature,
arglist: str,
multi_line_parameter_list: bool = False,
trailing_comma: bool = True,
) -> None:
"""'Parse' a list of arguments separated by commas.

Expand All @@ -532,6 +543,7 @@ def _pseudo_parse_arglist(
"""
paramlist = addnodes.desc_parameterlist()
paramlist['multi_line_parameter_list'] = multi_line_parameter_list
paramlist['multi_line_trailing_comma'] = trailing_comma
stack: list[Element] = [paramlist]
try:
for argument in arglist.split(','):
Expand Down
27 changes: 23 additions & 4 deletions sphinx/domains/python/_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
and (sig_len - (arglist_span[1] - arglist_span[0])) > max_len > 0
)

trailing_comma = self.env.config.python_trailing_comma_in_multi_line_signatures
sig_prefix = self.get_signature_prefix(sig)
if sig_prefix:
if type(sig_prefix) is str:
Expand All @@ -332,7 +333,10 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
if tp_list:
try:
signode += _parse_type_list(
tp_list, self.env, multi_line_type_parameter_list
tp_list,
self.env,
multi_line_type_parameter_list,
trailing_comma,
)
except Exception as exc:
logger.warning(
Expand All @@ -341,19 +345,34 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]

if arglist:
try:
signode += _parse_arglist(arglist, self.env, multi_line_parameter_list)
signode += _parse_arglist(
arglist,
self.env,
multi_line_parameter_list,
trailing_comma,
)
except SyntaxError:
# fallback to parse arglist original parser
# (this may happen if the argument list is incorrectly used
# as a list of bases when documenting a class)
# it supports to represent optional arguments (ex. "func(foo [, bar])")
_pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
_pseudo_parse_arglist(
signode,
arglist,
multi_line_parameter_list,
trailing_comma,
)
except (NotImplementedError, ValueError) as exc:
# duplicated parameter names raise ValueError and not a SyntaxError
logger.warning(
'could not parse arglist (%r): %s', arglist, exc, location=signode
)
_pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
_pseudo_parse_arglist(
signode,
arglist,
multi_line_parameter_list,
trailing_comma,
)
else:
if self.needs_arglist():
# for callables, add an empty parameter list
Expand Down
21 changes: 15 additions & 6 deletions sphinx/writers/html5.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = node.child_text_separator
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
self.trailing_comma = node.get('multi_line_trailing_comma', False)
if self.multi_line_parameter_list:
self.body.append('\n\n')
self.body.append(self.starttag(node, 'dl'))
Expand Down Expand Up @@ -238,7 +239,8 @@ def depart_desc_parameter(self, node: Element) -> None:
or is_required
and (is_last_group or next_is_required)
):
self.body.append(self.param_separator)
if not is_last_group or opt_param_left_at_level or self.trailing_comma:
self.body.append(self.param_separator)
self.body.append('</dd>\n')

elif self.required_params_left:
Expand Down Expand Up @@ -281,19 +283,26 @@ def visit_desc_optional(self, node: Element) -> None:

def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
level = self.optional_param_level
if self.multi_line_parameter_list:
# If it's the first time we go down one level, add the separator
# before the bracket.
if self.optional_param_level == self.max_optional_param_level - 1:
max_level = self.max_optional_param_level
len_lirp = len(self.list_is_required_param)
is_last_group = self.param_group_index + 1 == len_lirp
# If it's the first time we go down one level, add the separator before the
# bracket, except if this is the last parameter and the parameter list
# should not feature a trailing comma.
if level == max_level - 1 and (
not is_last_group or level > 0 or self.trailing_comma
):
self.body.append(self.param_separator)
self.body.append('<span class="optional">]</span>')
# End the line if we have just closed the last bracket of this
# optional parameter group.
if self.optional_param_level == 0:
if level == 0:
self.body.append('</dd>\n')
else:
self.body.append('<span class="optional">]</span>')
if self.optional_param_level == 0:
if level == 0:
self.param_group_index += 1

def visit_desc_annotation(self, node: Element) -> None:
Expand Down
16 changes: 12 additions & 4 deletions sphinx/writers/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = r'\sphinxparamcomma '
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
self.trailing_comma = node.get('multi_line_trailing_comma', False)

def visit_desc_parameterlist(self, node: Element) -> None:
if self.has_tp_list:
Expand Down Expand Up @@ -1013,7 +1014,7 @@ def _depart_sig_parameter(self, node: Element) -> None:
if (
opt_param_left_at_level
or is_required
and (is_last_group or next_is_required)
and (next_is_required or self.trailing_comma)
):
self.body.append(self.param_separator)

Expand Down Expand Up @@ -1055,13 +1056,20 @@ def visit_desc_optional(self, node: Element) -> None:

def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
level = self.optional_param_level
if self.multi_line_parameter_list:
max_level = self.max_optional_param_level
len_lirp = len(self.list_is_required_param)
is_last_group = self.param_group_index + 1 == len_lirp
# If it's the first time we go down one level, add the separator before the
# bracket.
if self.optional_param_level == self.max_optional_param_level - 1:
# bracket, except if this is the last parameter and the parameter list
# should not feature a trailing comma.
if level == max_level - 1 and (
not is_last_group or level > 0 or self.trailing_comma
):
self.body.append(self.param_separator)
self.body.append('}')
if self.optional_param_level == 0:
if level == 0:
self.param_group_index += 1

def visit_desc_annotation(self, node: Element) -> None:
Expand Down
19 changes: 14 additions & 5 deletions sphinx/writers/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = ', '
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
self.trailing_comma = node.get('multi_line_trailing_comma', False)
if self.multi_line_parameter_list:
self.param_separator = self.param_separator.rstrip()
self.context.append(sig_close_paren)
Expand Down Expand Up @@ -699,7 +700,8 @@ def visit_desc_parameter(self, node: Element) -> None:
or is_required
and (is_last_group or next_is_required)
):
self.add_text(self.param_separator)
if not is_last_group or opt_param_left_at_level or self.trailing_comma:
self.add_text(self.param_separator)
self.end_state(wrap=False, end=None)

elif self.required_params_left:
Expand Down Expand Up @@ -740,20 +742,27 @@ def visit_desc_optional(self, node: Element) -> None:

def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
level = self.optional_param_level
if self.multi_line_parameter_list:
max_level = self.max_optional_param_level
len_lirp = len(self.list_is_required_param)
is_last_group = self.param_group_index + 1 == len_lirp
# If it's the first time we go down one level, add the separator before the
# bracket.
if self.optional_param_level == self.max_optional_param_level - 1:
# bracket, except if this is the last parameter and the parameter list
# should not feature a trailing comma.
if level == max_level - 1 and (
not is_last_group or level > 0 or self.trailing_comma
):
self.add_text(self.param_separator)
self.add_text(']')
# End the line if we have just closed the last bracket of this group of
# optional parameters.
if self.optional_param_level == 0:
if level == 0:
self.end_state(wrap=False, end=None)

else:
self.add_text(']')
if self.optional_param_level == 0:
if level == 0:
self.param_group_index += 1

def visit_desc_annotation(self, node: Element) -> None:
Expand Down
Loading
Loading