summaryrefslogtreecommitdiff
path: root/sphinx
diff options
context:
space:
mode:
authorTLouf <31036680+TLouf@users.noreply.github.com>2023-05-11 15:28:57 +0200
committerGitHub <noreply@github.com>2023-05-11 14:28:57 +0100
commit86b07d4a97a225e79150d14e25a768ebc4c087cc (patch)
tree2c0c8691fff120604b9071cb24019ed1d49986a6 /sphinx
parentc73628dfcac844f89198ccd805e8e35609b37636 (diff)
downloadsphinx-git-86b07d4a97a225e79150d14e25a768ebc4c087cc.tar.gz
Allow multi-line object description signatures (#11011)
Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> Co-authored-by: Jean-François B <2589111+jfbu@users.noreply.github.com> Co-authored-by: TLouf <loufthomas@gmail.com>
Diffstat (limited to 'sphinx')
-rw-r--r--sphinx/addnodes.py7
-rw-r--r--sphinx/config.py2
-rw-r--r--sphinx/domains/c.py22
-rw-r--r--sphinx/domains/cpp.py22
-rw-r--r--sphinx/domains/javascript.py17
-rw-r--r--sphinx/domains/python.py27
-rw-r--r--sphinx/texinputs/sphinxlatexobjects.sty21
-rw-r--r--sphinx/texinputs/sphinxlatexstyletext.sty8
-rw-r--r--sphinx/themes/basic/static/basic.css_t10
-rw-r--r--sphinx/themes/epub/static/epub.css_t10
-rw-r--r--sphinx/writers/html5.py84
-rw-r--r--sphinx/writers/latex.py68
-rw-r--r--sphinx/writers/text.py87
13 files changed, 350 insertions, 35 deletions
diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py
index 44655d9be..e92d32a0e 100644
--- a/sphinx/addnodes.py
+++ b/sphinx/addnodes.py
@@ -246,7 +246,12 @@ class desc_returns(desc_type):
class desc_parameterlist(nodes.Part, nodes.Inline, nodes.FixedTextElement):
- """Node for a general parameter list."""
+ """Node for a general parameter list.
+
+ 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.
+ """
child_text_separator = ', '
def astext(self):
diff --git a/sphinx/config.py b/sphinx/config.py
index ad7c3b568..a4e661934 100644
--- a/sphinx/config.py
+++ b/sphinx/config.py
@@ -137,7 +137,7 @@ class Config:
'numfig': (False, 'env', []),
'numfig_secnum_depth': (1, 'env', []),
'numfig_format': ({}, 'env', []), # will be initialized in init_numfig_format()
-
+ 'maximum_signature_line_length': (None, 'env', {int, None}),
'math_number_all': (False, 'env', []),
'math_eqref_format': (None, 'env', [str]),
'math_numfig': (True, 'env', []),
diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py
index c583a770d..0bb505fba 100644
--- a/sphinx/domains/c.py
+++ b/sphinx/domains/c.py
@@ -727,9 +727,19 @@ class ASTParameters(ASTBase):
def describe_signature(self, signode: TextElement, mode: str,
env: BuildEnvironment, symbol: Symbol) -> None:
verify_description_mode(mode)
+ multi_line_parameter_list = False
+ test_node: Element = signode
+ while test_node.parent:
+ if not isinstance(test_node, addnodes.desc_signature):
+ test_node = test_node.parent
+ continue
+ multi_line_parameter_list = test_node.get('multi_line_parameter_list', False)
+ break
+
# only use the desc_parameterlist for the outer list, not for inner lists
if mode == 'lastIsName':
paramlist = addnodes.desc_parameterlist()
+ paramlist['multi_line_parameter_list'] = multi_line_parameter_list
for arg in self.args:
param = addnodes.desc_parameter('', '', noemph=True)
arg.describe_signature(param, 'param', env, symbol=symbol)
@@ -3153,6 +3163,7 @@ class CObject(ObjectDescription[ASTDeclaration]):
option_spec: OptionSpec = {
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
+ 'single-line-parameter-list': directives.flag,
}
def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None:
@@ -3258,6 +3269,14 @@ class CObject(ObjectDescription[ASTDeclaration]):
def handle_signature(self, sig: str, signode: TextElement) -> ASTDeclaration:
parentSymbol: Symbol = self.env.temp_data['c:parent_symbol']
+ max_len = (self.env.config.c_maximum_signature_line_length
+ or self.env.config.maximum_signature_line_length
+ or 0)
+ signode['multi_line_parameter_list'] = (
+ 'single-line-parameter-list' not in self.options
+ and (len(sig) > max_len > 0)
+ )
+
parser = DefinitionParser(sig, location=signode, config=self.env.config)
try:
ast = self.parse_definition(parser)
@@ -3866,11 +3885,12 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value("c_id_attributes", [], 'env')
app.add_config_value("c_paren_attributes", [], 'env')
app.add_config_value("c_extra_keywords", _macroKeywords, 'env')
+ app.add_config_value("c_maximum_signature_line_length", None, 'env', types={int, None})
app.add_post_transform(AliasTransform)
return {
'version': 'builtin',
- 'env_version': 2,
+ 'env_version': 3,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py
index a7d16aa06..41f2bd076 100644
--- a/sphinx/domains/cpp.py
+++ b/sphinx/domains/cpp.py
@@ -2142,9 +2142,19 @@ class ASTParametersQualifiers(ASTBase):
def describe_signature(self, signode: TextElement, mode: str,
env: BuildEnvironment, symbol: Symbol) -> None:
verify_description_mode(mode)
+ multi_line_parameter_list = False
+ test_node: Element = signode
+ while test_node.parent:
+ if not isinstance(test_node, addnodes.desc_signature):
+ test_node = test_node.parent
+ continue
+ multi_line_parameter_list = test_node.get('multi_line_parameter_list', False)
+ break
+
# only use the desc_parameterlist for the outer list, not for inner lists
if mode == 'lastIsName':
paramlist = addnodes.desc_parameterlist()
+ paramlist['multi_line_parameter_list'] = multi_line_parameter_list
for arg in self.args:
param = addnodes.desc_parameter('', '', noemph=True)
arg.describe_signature(param, 'param', env, symbol=symbol)
@@ -7192,6 +7202,7 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
'tparam-line-spec': directives.flag,
+ 'single-line-parameter-list': directives.flag,
}
def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None:
@@ -7348,6 +7359,14 @@ class CPPObject(ObjectDescription[ASTDeclaration]):
def handle_signature(self, sig: str, signode: desc_signature) -> ASTDeclaration:
parentSymbol: Symbol = self.env.temp_data['cpp:parent_symbol']
+ max_len = (self.env.config.cpp_maximum_signature_line_length
+ or self.env.config.maximum_signature_line_length
+ or 0)
+ signode['multi_line_parameter_list'] = (
+ 'single-line-parameter-list' not in self.options
+ and (len(sig) > max_len > 0)
+ )
+
parser = DefinitionParser(sig, location=signode, config=self.env.config)
try:
ast = self.parse_definition(parser)
@@ -8140,6 +8159,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value("cpp_index_common_prefix", [], 'env')
app.add_config_value("cpp_id_attributes", [], 'env')
app.add_config_value("cpp_paren_attributes", [], 'env')
+ app.add_config_value("cpp_maximum_signature_line_length", None, 'env', types={int, None})
app.add_post_transform(AliasTransform)
# debug stuff
@@ -8154,7 +8174,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
return {
'version': 'builtin',
- 'env_version': 8,
+ 'env_version': 9,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py
index 093e291ca..c6baab8a9 100644
--- a/sphinx/domains/javascript.py
+++ b/sphinx/domains/javascript.py
@@ -43,6 +43,7 @@ class JSObject(ObjectDescription[Tuple[str, str]]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
+ 'single-line-parameter-list': directives.flag,
}
def get_display_prefix(self) -> list[Node]:
@@ -88,6 +89,14 @@ class JSObject(ObjectDescription[Tuple[str, str]]):
signode['object'] = prefix
signode['fullname'] = fullname
+ max_len = (self.env.config.javascript_maximum_signature_line_length
+ or self.env.config.maximum_signature_line_length
+ or 0)
+ multi_line_parameter_list = (
+ 'single-line-parameter-list' not in self.options
+ and (len(sig) > max_len > 0)
+ )
+
display_prefix = self.get_display_prefix()
if display_prefix:
signode += addnodes.desc_annotation('', '', *display_prefix)
@@ -108,7 +117,7 @@ class JSObject(ObjectDescription[Tuple[str, str]]):
if not arglist:
signode += addnodes.desc_parameterlist()
else:
- _pseudo_parse_arglist(signode, arglist)
+ _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
return fullname, prefix
def _object_hierarchy_parts(self, sig_node: desc_signature) -> tuple[str, ...]:
@@ -473,10 +482,12 @@ class JavaScriptDomain(Domain):
def setup(app: Sphinx) -> dict[str, Any]:
app.add_domain(JavaScriptDomain)
-
+ app.add_config_value(
+ 'javascript_maximum_signature_line_length', None, 'env', types={int, None},
+ )
return {
'version': 'builtin',
- 'env_version': 2,
+ 'env_version': 3,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index eef78aa80..3fda52703 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -258,10 +258,11 @@ def _parse_annotation(annotation: str, env: BuildEnvironment | None) -> list[Nod
def _parse_arglist(
- arglist: str, env: BuildEnvironment | None = None,
+ arglist: str, env: BuildEnvironment | None = None, multi_line_parameter_list: bool = False,
) -> 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
sig = signature_from_str('(%s)' % arglist)
last_kind = None
for param in sig.parameters.values():
@@ -309,7 +310,9 @@ def _parse_arglist(
return params
-def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
+def _pseudo_parse_arglist(
+ signode: desc_signature, arglist: str, multi_line_parameter_list: bool = False,
+) -> None:
""""Parse" a list of arguments separated by commas.
Arguments can have "optional" annotations given by enclosing them in
@@ -317,6 +320,7 @@ def _pseudo_parse_arglist(signode: desc_signature, arglist: str) -> None:
string literal (e.g. default argument value).
"""
paramlist = addnodes.desc_parameterlist()
+ paramlist['multi_line_parameter_list'] = multi_line_parameter_list
stack: list[Element] = [paramlist]
try:
for argument in arglist.split(','):
@@ -459,6 +463,7 @@ class PyObject(ObjectDescription[Tuple[str, str]]):
'noindex': directives.flag,
'noindexentry': directives.flag,
'nocontentsentry': directives.flag,
+ 'single-line-parameter-list': directives.flag,
'module': directives.unchanged,
'canonical': directives.unchanged,
'annotation': directives.unchanged,
@@ -541,6 +546,14 @@ class PyObject(ObjectDescription[Tuple[str, str]]):
signode['class'] = classname
signode['fullname'] = fullname
+ max_len = (self.env.config.python_maximum_signature_line_length
+ or self.env.config.maximum_signature_line_length
+ or 0)
+ multi_line_parameter_list = (
+ 'single-line-parameter-list' not in self.options
+ and (len(sig) > max_len > 0)
+ )
+
sig_prefix = self.get_signature_prefix(sig)
if sig_prefix:
if type(sig_prefix) is str:
@@ -559,15 +572,15 @@ class PyObject(ObjectDescription[Tuple[str, str]]):
signode += addnodes.desc_name(name, name)
if arglist:
try:
- signode += _parse_arglist(arglist, self.env)
+ signode += _parse_arglist(arglist, self.env, multi_line_parameter_list)
except SyntaxError:
# fallback to parse arglist original parser.
# it supports to represent optional arguments (ex. "func(foo [, bar])")
- _pseudo_parse_arglist(signode, arglist)
+ _pseudo_parse_arglist(signode, arglist, 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)
+ _pseudo_parse_arglist(signode, arglist, multi_line_parameter_list)
else:
if self.needs_arglist():
# for callables, add an empty parameter list
@@ -1505,13 +1518,15 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_domain(PythonDomain)
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_display_short_literal_types', False, 'env')
app.connect('object-description-transform', filter_meta_fields)
app.connect('missing-reference', builtin_resolver, priority=900)
return {
'version': 'builtin',
- 'env_version': 3,
+ 'env_version': 4,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
diff --git a/sphinx/texinputs/sphinxlatexobjects.sty b/sphinx/texinputs/sphinxlatexobjects.sty
index b4ff1f9d0..a2038a9f1 100644
--- a/sphinx/texinputs/sphinxlatexobjects.sty
+++ b/sphinx/texinputs/sphinxlatexobjects.sty
@@ -146,6 +146,27 @@
\item[{#1\sphinxcode{(}\py@sigparams{#2}{#3}\strut}]
\pysigadjustitemsep
}
+
+\def\sphinxoptionalextraspace{0.5mm}
+\newcommand{\pysigwithonelineperarg}[3]{%
+ % render each argument on its own line
+ \item[#1\sphinxcode{(}\strut]
+ \leavevmode\par\nopagebreak
+ % this relies on \pysigstartsignatures having set \parskip to zero
+ \begingroup
+ \let\sphinxparamcomma\sphinxparamcommaoneperline
+ \def\sphinxoptionalhook{\ifvmode\else\kern\sphinxoptionalextraspace\relax\fi}%
+ % The very first \sphinxparam should not emit a \par hence a complication
+ % with a group and global definition here as it may occur in a \sphinxoptional
+ \global\let\spx@sphinxparam\sphinxparam
+ \gdef\sphinxparam{\gdef\sphinxparam{\par\spx@sphinxparam}\spx@sphinxparam}%
+ #2\par
+ \endgroup
+ \global\let\sphinxparam\spx@sphinxparam
+ % fulllineitems sets \labelwidth to be like \leftmargin
+ \nopagebreak\noindent\kern-\labelwidth\sphinxcode{)}{#3}
+ \pysigadjustitemsep
+}
\newcommand{\pysigadjustitemsep}{%
% adjust \itemsep to control the separation with the next signature
% sharing common description
diff --git a/sphinx/texinputs/sphinxlatexstyletext.sty b/sphinx/texinputs/sphinxlatexstyletext.sty
index 913bc8210..292facc91 100644
--- a/sphinx/texinputs/sphinxlatexstyletext.sty
+++ b/sphinx/texinputs/sphinxlatexstyletext.sty
@@ -58,7 +58,8 @@
\protected\def\sphinxparam#1{\emph{#1}}
% \optional is used for ``[, arg]``, i.e. desc_optional nodes.
\long\protected\def\sphinxoptional#1{%
- {\textnormal{\Large[}}{#1}\hspace{0.5mm}{\textnormal{\Large]}}}
+ {\sphinxoptionalhook\textnormal{\Large[}}{#1}\hspace{0.5mm}{\textnormal{\Large]}}}
+\let\sphinxoptionalhook\empty
% additional customizable styling
\def\sphinxstyleindexentry #1{\texttt{#1}}
@@ -112,6 +113,11 @@
% Special characters
%
+\def\sphinxparamcomma{, }% by default separate parameters with comma + space
+% If the signature is rendered with one line per param, this wil be used
+% instead (this \texttt makes the comma slightly more distinctive).
+\def\sphinxparamcommaoneperline{\texttt{,}}
+%
% The \kern\z@ is to prevent en-dash and em-dash TeX ligatures.
% A linebreak can occur after the dash in regular text (this is
% normal behaviour of "-" in TeX, it is not related to \kern\z@).
diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t
index 9d5e4419d..9ae180267 100644
--- a/sphinx/themes/basic/static/basic.css_t
+++ b/sphinx/themes/basic/static/basic.css_t
@@ -670,6 +670,16 @@ dd {
margin-left: 30px;
}
+.sig dd {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+.sig dl {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
diff --git a/sphinx/themes/epub/static/epub.css_t b/sphinx/themes/epub/static/epub.css_t
index 767d558be..15938cdc5 100644
--- a/sphinx/themes/epub/static/epub.css_t
+++ b/sphinx/themes/epub/static/epub.css_t
@@ -458,6 +458,11 @@ dl {
margin-bottom: 15px;
}
+.sig dl {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
dd p {
margin-top: 0px;
}
@@ -472,6 +477,11 @@ dd {
margin-left: 30px;
}
+.sig dd {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
dt:target, .highlighted {
background-color: #ddd;
}
diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py
index ab26bab1e..e7d932286 100644
--- a/sphinx/writers/html5.py
+++ b/sphinx/writers/html5.py
@@ -150,14 +150,26 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
def visit_desc_parameterlist(self, node: Element) -> None:
self.body.append('<span class="sig-paren">(</span>')
- self.first_param = 1
+ self.is_first_param = True
self.optional_param_level = 0
+ self.params_left_at_level = 0
+ self.param_group_index = 0
+ # Counts as what we call a parameter group either a required parameter, or a
+ # set of contiguous optional ones.
+ self.list_is_required_param = [isinstance(c, addnodes.desc_parameter)
+ for c in node.children]
# How many required parameters are left.
- self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
- for c in node.children])
+ 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)
+ 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'):
+ self.body.append('</dl>\n\n')
self.body.append('<span class="sig-paren">)</span>')
# If required parameters are still to come, then put the comma after
@@ -167,28 +179,82 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
# foo([a, ]b, c[, d])
#
def visit_desc_parameter(self, node: Element) -> None:
- if self.first_param:
- self.first_param = 0
- elif not self.required_params_left:
+ on_separate_line = self.multi_line_parameter_list
+ if on_separate_line and not (self.is_first_param and self.optional_param_level > 0):
+ 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:
self.body.append(self.param_separator)
if self.optional_param_level == 0:
self.required_params_left -= 1
+ else:
+ self.params_left_at_level -= 1
if not node.hasattr('noemph'):
self.body.append('<em class="sig-param">')
def depart_desc_parameter(self, node: Element) -> None:
if not node.hasattr('noemph'):
self.body.append('</em>')
- if self.required_params_left:
+ is_required = self.list_is_required_param[self.param_group_index]
+ 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
+ and self.list_is_required_param[self.param_group_index + 1]
+ )
+ 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('</dd>\n')
+
+ elif self.required_params_left:
self.body.append(self.param_separator)
+ if is_required:
+ self.param_group_index += 1
+
def visit_desc_optional(self, node: Element) -> None:
+ self.params_left_at_level = sum([isinstance(c, addnodes.desc_parameter)
+ for c in node.children])
self.optional_param_level += 1
- self.body.append('<span class="optional">[</span>')
+ self.max_optional_param_level = self.optional_param_level
+ if self.multi_line_parameter_list:
+ # If the first parameter is optional, start a new line and open the bracket.
+ if self.is_first_param:
+ self.body.append(self.starttag(node, 'dd', ''))
+ self.body.append('<span class="optional">[</span>')
+ # Else, if there remains at least one required parameter, append the
+ # parameter separator, open a new bracket, and end the line.
+ elif self.required_params_left:
+ self.body.append(self.param_separator)
+ self.body.append('<span class="optional">[</span>')
+ self.body.append('</dd>\n')
+ # Else, open a new bracket, append the parameter separator,
+ # and end the line.
+ else:
+ self.body.append('<span class="optional">[</span>')
+ self.body.append(self.param_separator)
+ self.body.append('</dd>\n')
+ else:
+ self.body.append('<span class="optional">[</span>')
def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
- self.body.append('<span class="optional">]</span>')
+ 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:
+ 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:
+ self.body.append('</dd>\n')
+ else:
+ self.body.append('<span class="optional">]</span>')
+ if self.optional_param_level == 0:
+ self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
self.body.append(self.starttag(node, 'em', '', CLASS='property'))
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index e7d31b70e..37c73ae5a 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -702,7 +702,10 @@ class LaTeXTranslator(SphinxTranslator):
def _visit_signature_line(self, node: Element) -> None:
for child in node:
if isinstance(child, addnodes.desc_parameterlist):
- self.body.append(CR + r'\pysiglinewithargsret{')
+ if child.get('multi_line_parameter_list'):
+ self.body.append(CR + r'\pysigwithonelineperarg{')
+ else:
+ self.body.append(CR + r'\pysiglinewithargsret{')
break
else:
self.body.append(CR + r'\pysigline{')
@@ -784,29 +787,82 @@ class LaTeXTranslator(SphinxTranslator):
def visit_desc_parameterlist(self, node: Element) -> None:
# close name, open parameterlist
self.body.append('}{')
- self.first_param = 1
+ self.is_first_param = True
+ self.optional_param_level = 0
+ self.params_left_at_level = 0
+ self.param_group_index = 0
+ # Counts as what we call a parameter group either a required parameter, or a
+ # set of contiguous optional ones.
+ self.list_is_required_param = [isinstance(c, addnodes.desc_parameter)
+ for c in node.children]
+ # How many required parameters are left.
+ 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)
def depart_desc_parameterlist(self, node: Element) -> None:
# close parameterlist, open return annotation
self.body.append('}{')
def visit_desc_parameter(self, node: Element) -> None:
- if not self.first_param:
- self.body.append(', ')
+ if self.is_first_param:
+ self.is_first_param = False
+ 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
else:
- self.first_param = 0
+ self.params_left_at_level -= 1
if not node.hasattr('noemph'):
self.body.append(r'\sphinxparam{')
def depart_desc_parameter(self, node: Element) -> None:
if not node.hasattr('noemph'):
self.body.append('}')
+ is_required = self.list_is_required_param[self.param_group_index]
+ 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
+ and self.list_is_required_param[self.param_group_index + 1]
+ )
+ 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)
+
+ elif self.required_params_left:
+ self.body.append(self.param_separator)
+
+ if is_required:
+ self.param_group_index += 1
def visit_desc_optional(self, node: Element) -> None:
- self.body.append(r'\sphinxoptional{')
+ self.params_left_at_level = sum([isinstance(c, addnodes.desc_parameter)
+ for c in node.children])
+ self.optional_param_level += 1
+ self.max_optional_param_level = self.optional_param_level
+ if self.multi_line_parameter_list:
+ if self.is_first_param:
+ self.body.append(r'\sphinxoptional{')
+ elif self.required_params_left:
+ self.body.append(self.param_separator)
+ self.body.append(r'\sphinxoptional{')
+ else:
+ self.body.append(r'\sphinxoptional{')
+ self.body.append(self.param_separator)
+ else:
+ self.body.append(r'\sphinxoptional{')
def depart_desc_optional(self, node: Element) -> None:
+ self.optional_param_level -= 1
+ 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:
+ self.body.append(self.param_separator)
self.body.append('}')
+ if self.optional_param_level == 0:
+ self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
self.body.append(r'\sphinxbfcode{\sphinxupquote{')
diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py
index 3bce03ac6..8e3d9df24 100644
--- a/sphinx/writers/text.py
+++ b/sphinx/writers/text.py
@@ -594,24 +594,99 @@ class TextTranslator(SphinxTranslator):
def visit_desc_parameterlist(self, node: Element) -> None:
self.add_text('(')
- self.first_param = 1
+ self.is_first_param = True
+ self.optional_param_level = 0
+ self.params_left_at_level = 0
+ self.param_group_index = 0
+ # Counts as what we call a parameter group are either a required parameter, or a
+ # set of contiguous optional ones.
+ self.list_is_required_param = [isinstance(c, addnodes.desc_parameter)
+ for c in node.children]
+ 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:
- if not self.first_param:
- self.add_text(', ')
+ on_separate_line = self.multi_line_parameter_list
+ if on_separate_line and not (self.is_first_param and self.optional_param_level > 0):
+ self.new_state()
+ if self.is_first_param:
+ self.is_first_param = False
+ elif not on_separate_line and not self.required_params_left:
+ self.add_text(self.param_separator)
+ if self.optional_param_level == 0:
+ self.required_params_left -= 1
else:
- self.first_param = 0
+ self.params_left_at_level -= 1
+
self.add_text(node.astext())
+
+ is_required = self.list_is_required_param[self.param_group_index]
+ if on_separate_line:
+ is_last_group = self.param_group_index + 1 == len(self.list_is_required_param)
+ next_is_required = (
+ not is_last_group
+ and self.list_is_required_param[self.param_group_index + 1]
+ )
+ 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)
+
+ elif self.required_params_left:
+ self.add_text(self.param_separator)
+
+ if is_required:
+ self.param_group_index += 1
raise nodes.SkipNode
def visit_desc_optional(self, node: Element) -> None:
- self.add_text('[')
+ self.params_left_at_level = sum([isinstance(c, addnodes.desc_parameter)
+ for c in node.children])
+ self.optional_param_level += 1
+ self.max_optional_param_level = self.optional_param_level
+ if self.multi_line_parameter_list:
+ # If the first parameter is optional, start a new line and open the bracket.
+ if self.is_first_param:
+ 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.
+ 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:
+ self.add_text('[')
+ self.add_text(self.param_separator)
+ self.end_state(wrap=False, end=None)
+ else:
+ self.add_text('[')
def depart_desc_optional(self, node: Element) -> None:
- self.add_text(']')
+ self.optional_param_level -= 1
+ 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:
+ 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:
+ self.end_state(wrap=False, end=None)
+
+ else:
+ self.add_text(']')
+ if self.optional_param_level == 0:
+ self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
pass