diff options
author | TLouf <31036680+TLouf@users.noreply.github.com> | 2023-05-11 15:28:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-11 14:28:57 +0100 |
commit | 86b07d4a97a225e79150d14e25a768ebc4c087cc (patch) | |
tree | 2c0c8691fff120604b9071cb24019ed1d49986a6 /sphinx | |
parent | c73628dfcac844f89198ccd805e8e35609b37636 (diff) | |
download | sphinx-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.py | 7 | ||||
-rw-r--r-- | sphinx/config.py | 2 | ||||
-rw-r--r-- | sphinx/domains/c.py | 22 | ||||
-rw-r--r-- | sphinx/domains/cpp.py | 22 | ||||
-rw-r--r-- | sphinx/domains/javascript.py | 17 | ||||
-rw-r--r-- | sphinx/domains/python.py | 27 | ||||
-rw-r--r-- | sphinx/texinputs/sphinxlatexobjects.sty | 21 | ||||
-rw-r--r-- | sphinx/texinputs/sphinxlatexstyletext.sty | 8 | ||||
-rw-r--r-- | sphinx/themes/basic/static/basic.css_t | 10 | ||||
-rw-r--r-- | sphinx/themes/epub/static/epub.css_t | 10 | ||||
-rw-r--r-- | sphinx/writers/html5.py | 84 | ||||
-rw-r--r-- | sphinx/writers/latex.py | 68 | ||||
-rw-r--r-- | sphinx/writers/text.py | 87 |
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 |