diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py
index e92d32a0ef8..7061f67c09d 100644
--- a/sphinx/addnodes.py
+++ b/sphinx/addnodes.py
@@ -259,7 +259,11 @@ def astext(self):
class desc_parameter(nodes.Part, nodes.Inline, nodes.FixedTextElement):
- """Node for a single parameter."""
+ """Node for a single parameter.
+
+ If the parent `desc_parameterlist` node has ``multi_line_parameter_list = True``,
+ set ``on_new_line = True`` to display on a new line.
+ """
class desc_optional(nodes.Part, nodes.Inline, nodes.FixedTextElement):
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index 3fda5270351..c95558af948 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -265,6 +265,13 @@ def _parse_arglist(
params['multi_line_parameter_list'] = multi_line_parameter_list
sig = signature_from_str('(%s)' % arglist)
last_kind = None
+
+ if multi_line_parameter_list and env is not None:
+ max_param_line_length = env.config.python_maximum_signature_line_length
+ single_param_per_line = env.config.python_single_param_per_line_in_multiline_signatures
+ line_length = 0
+ is_first_param = True
+
for param in sig.parameters.values():
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: /
@@ -300,6 +307,13 @@ def _parse_arglist(
node += nodes.inline('', param.default, classes=['default_value'],
support_smartquotes=False)
+ if multi_line_parameter_list: # TODO: for every params +=... -> define func?
+ param_str_length = len(node.astext()) + 2
+ line_length += param_str_length
+ if single_param_per_line or is_first_param or line_length > max_param_line_length:
+ is_first_param = False
+ node['on_new_line'] = True
+ line_length = param_str_length
params += node
last_kind = param.kind
@@ -311,7 +325,10 @@ def _parse_arglist(
def _pseudo_parse_arglist(
- signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False,
+ signode: desc_signature,
+ arglist: str,
+ env: BuildEnvironment | None = None,
+ multi_line_parameter_list: bool = False,
) -> None:
""""Parse" a list of arguments separated by commas.
@@ -321,10 +338,17 @@ def _pseudo_parse_arglist(
"""
paramlist = addnodes.desc_parameterlist()
paramlist['multi_line_parameter_list'] = multi_line_parameter_list
+ if multi_line_parameter_list and env is not None:
+ max_param_line_length = env.config.python_maximum_signature_line_length
+ single_param_per_line = env.config.python_single_param_per_line_in_multiline_signatures
+ line_length = 0
+ is_first_param = True
+
stack: list[Element] = [paramlist]
try:
for argument in arglist.split(','):
argument = argument.strip()
+ param_str_length = len(argument) + 2 # TODO: + closing optionals?
ends_open = ends_close = 0
while argument.startswith('['):
stack.append(addnodes.desc_optional())
@@ -340,8 +364,17 @@ def _pseudo_parse_arglist(
ends_open += 1
argument = argument[:-1].strip()
if argument:
- stack[-1] += addnodes.desc_parameter(
- '', '', addnodes.desc_sig_name(argument, argument))
+ node = addnodes.desc_parameter(
+ '', '', addnodes.desc_sig_name(argument, argument),
+ )
+ if multi_line_parameter_list:
+ line_length += param_str_length
+ goes_over_max_line_length = line_length > max_param_line_length
+ if single_param_per_line or is_first_param or goes_over_max_line_length:
+ is_first_param = False
+ node['on_new_line'] = True
+ line_length = param_str_length
+ stack[-1] += node
while ends_open:
stack.append(addnodes.desc_optional())
stack[-2] += stack[-1]
@@ -576,11 +609,11 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]
except SyntaxError:
# fallback to parse arglist original parser.
# it supports to represent optional arguments (ex. "func(foo [, bar])")
- _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
+ _pseudo_parse_arglist(signode, arglist, self.env, multi_line_parameter_list)
except NotImplementedError as exc:
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, self.env, multi_line_parameter_list)
else:
if self.needs_arglist():
# for callables, add an empty parameter list
@@ -1520,6 +1553,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('python_use_unqualified_type_names', False, 'env')
app.add_config_value('python_maximum_signature_line_length', None, 'env',
types={int, None})
+ app.add_config_value('python_single_param_per_line_in_multiline_signatures', True, 'env')
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)
diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py
index e7d932286c5..e41d9084271 100644
--- a/sphinx/writers/html5.py
+++ b/sphinx/writers/html5.py
@@ -165,7 +165,6 @@ def visit_desc_parameterlist(self, node: Element) -> None:
if self.multi_line_parameter_list:
self.body.append('\n\n')
self.body.append(self.starttag(node, 'dl'))
- self.param_separator = self.param_separator.rstrip()
def depart_desc_parameterlist(self, node: Element) -> None:
if node.get('multi_line_parameter_list'):
@@ -179,12 +178,14 @@ def depart_desc_parameterlist(self, node: Element) -> None:
# foo([a, ]b, c[, d])
#
def visit_desc_parameter(self, node: Element) -> None:
- on_separate_line = self.multi_line_parameter_list
+ on_separate_line = node.get('on_new_line')
if on_separate_line and not (self.is_first_param and self.optional_param_level > 0):
+ if not self.is_first_param:
+ self.body.append('\n')
self.body.append(self.starttag(node, 'dd', ''))
if self.is_first_param:
self.is_first_param = False
- elif not on_separate_line and not self.required_params_left:
+ elif not self.multi_line_parameter_list and not self.required_params_left:
self.body.append(self.param_separator)
if self.optional_param_level == 0:
self.required_params_left -= 1
@@ -206,7 +207,8 @@ def depart_desc_parameter(self, node: Element) -> None:
opt_param_left_at_level = self.params_left_at_level > 0
if opt_param_left_at_level or is_required and (is_last_group or next_is_required):
self.body.append(self.param_separator)
- self.body.append('\n')
+ if not opt_param_left_at_level and is_last_group:
+ self.body.append('\n')
elif self.required_params_left:
self.body.append(self.param_separator)
@@ -225,17 +227,14 @@ def visit_desc_optional(self, node: Element) -> None:
self.body.append(self.starttag(node, 'dd', ''))
self.body.append('[')
# Else, if there remains at least one required parameter, append the
- # parameter separator, open a new bracket, and end the line.
+ # parameter separator and open a new bracket.
elif self.required_params_left:
self.body.append(self.param_separator)
self.body.append('[')
- self.body.append('\n')
- # Else, open a new bracket, append the parameter separator,
- # and end the line.
+ # Else, open a new bracket and append the parameter separator.
else:
self.body.append('[')
self.body.append(self.param_separator)
- self.body.append('\n')
else:
self.body.append('[')
@@ -248,8 +247,9 @@ def depart_desc_optional(self, node: Element) -> None:
self.body.append(self.param_separator)
self.body.append(']')
# End the line if we have just closed the last bracket of this
- # optional parameter group.
- if self.optional_param_level == 0:
+ # optional parameter group and there is no group left.
+ is_last_group = self.param_group_index + 1 == len(self.list_is_required_param)
+ if self.optional_param_level == 0 and is_last_group:
self.body.append('\n')
else:
self.body.append(']')
diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py
index 8e3d9df240d..303a37d3737 100644
--- a/sphinx/writers/text.py
+++ b/sphinx/writers/text.py
@@ -605,19 +605,19 @@ def visit_desc_parameterlist(self, node: Element) -> None:
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)
- if self.multi_line_parameter_list:
- self.param_separator = self.param_separator.rstrip()
def depart_desc_parameterlist(self, node: Element) -> None:
self.add_text(')')
def visit_desc_parameter(self, node: Element) -> None:
- on_separate_line = self.multi_line_parameter_list
+ on_separate_line = node.get('on_new_line')
if on_separate_line and not (self.is_first_param and self.optional_param_level > 0):
+ if not self.is_first_param:
+ self.end_state(wrap=False, end=None)
self.new_state()
if self.is_first_param:
self.is_first_param = False
- elif not on_separate_line and not self.required_params_left:
+ elif not self.multi_line_parameter_list and not self.required_params_left:
self.add_text(self.param_separator)
if self.optional_param_level == 0:
self.required_params_left -= 1
@@ -627,7 +627,7 @@ def visit_desc_parameter(self, node: Element) -> None:
self.add_text(node.astext())
is_required = self.list_is_required_param[self.param_group_index]
- if on_separate_line:
+ if self.multi_line_parameter_list:
is_last_group = self.param_group_index + 1 == len(self.list_is_required_param)
next_is_required = (
not is_last_group
@@ -636,7 +636,8 @@ def visit_desc_parameter(self, node: Element) -> None:
opt_param_left_at_level = self.params_left_at_level > 0
if opt_param_left_at_level or is_required and (is_last_group or next_is_required):
self.add_text(self.param_separator)
- self.end_state(wrap=False, end=None)
+ if not opt_param_left_at_level and is_last_group:
+ self.end_state(wrap=False, end=None)
elif self.required_params_left:
self.add_text(self.param_separator)
@@ -656,17 +657,14 @@ def visit_desc_optional(self, node: Element) -> None:
self.new_state()
self.add_text('[')
# Else, if there remains at least one required parameter, append the
- # parameter separator, open a new bracket, and end the line.
+ # parameter separator and open a new bracket.
elif self.required_params_left:
self.add_text(self.param_separator)
self.add_text('[')
- self.end_state(wrap=False, end=None)
- # Else, open a new bracket, append the parameter separator, and end the
- # line.
+ # Else, open a new bracket and append the parameter separator.
else:
self.add_text('[')
self.add_text(self.param_separator)
- self.end_state(wrap=False, end=None)
else:
self.add_text('[')
@@ -678,9 +676,10 @@ def depart_desc_optional(self, node: Element) -> None:
if self.optional_param_level == self.max_optional_param_level - 1:
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:
+ # End the line if we have just closed the last bracket of this
+ # optional parameter group and there is no group left.
+ is_last_group = self.param_group_index + 1 == len(self.list_is_required_param)
+ if self.optional_param_level == 0 and is_last_group:
self.end_state(wrap=False, end=None)
else: