diff --git a/CHANGES b/CHANGES index 9ac823cdb4c..07bc8157ef1 100644 --- a/CHANGES +++ b/CHANGES @@ -26,6 +26,13 @@ Features added generated HTML, using the CRC32 algorithm. * :meth:`~sphinx.application.Sphinx.require_sphinx` now allows the version requirement to be specified as ``(major, minor)``. +* #11011: Allow configuring a line-length limit for object signatures, via + :confval:`maximum_signature_line_length` and the domain-specific variants. + If the length of the signature (in characters) is greater than the configured + limit, each parameter in the signature will be split to its own logical line. + This behaviour may also be controlled by options on object description + directives, for example :rst:dir:`py:function:single-line-parameter-list`. + Patch by Thomas Louf, Adam Turner, and Jean-François Burnol. Bugs fixed ---------- diff --git a/doc/latex.rst b/doc/latex.rst index 50440342785..a451ae6a43c 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -1479,6 +1479,12 @@ Macros .. versionadded:: 6.2.0 ``\sphinxparam``, ``\sphinxsamedocref`` + .. versionadded:: 7.1.0 + ``\sphinxparamcomma`` which defaults to a comma followed by a space and + ``\sphinxparamcommaoneperline`` which is used for one-parameter-per-line + signatures (see :confval:`maximum_signature_line_length`). It defaults + to ``\texttt{,}`` to make these end-of-line separators more distinctive. + - More text styling: .. csv-table:: diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 23e9ba7ab36..1fc4c674b4c 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -675,6 +675,25 @@ General configuration If the value is a fully-qualified name of a custom Pygments style class, this is then used as custom style. +.. confval:: maximum_signature_line_length + + If a signature's length in characters exceeds the number set, each + parameter within the signature will be displayed on an individual logical + line. + + When ``None`` (the default), there is no maximum length and the entire + signature will be displayed on a single logical line. + + A 'logical line' is similar to a hard line break---builders or themes may + choose to 'soft wrap' a single logical line, and this setting does not affect + that behaviour. + + Domains may provide options to suppress any hard wrapping on an individual + object directive, such as seen in the C, C++, and Python domains (e.g. + :rst:dir:`py:function:single-line-parameter-list`). + + .. versionadded:: 7.1 + .. confval:: add_function_parentheses A boolean that decides whether parentheses are appended to function and @@ -2912,6 +2931,14 @@ Options for the C domain .. versionadded:: 4.0.3 +.. confval:: c_maximum_signature_line_length + + If a signature's length in characters exceeds the number set, each + parameter will be displayed on an individual logical line. This is a + domain-specific setting, overriding :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. _cpp-config: Options for the C++ domain @@ -2942,6 +2969,14 @@ Options for the C++ domain .. versionadded:: 1.5 +.. confval:: cpp_maximum_signature_line_length + + If a signature's length in characters exceeds the number set, each + parameter will be displayed on an individual logical line. This is a + domain-specific setting, overriding :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + Options for the Python domain ----------------------------- @@ -2984,6 +3019,25 @@ Options for the Python domain .. note:: This configuration is still in experimental +.. confval:: python_maximum_signature_line_length + + If a signature's length in characters exceeds the number set, each + argument will be displayed on an individual logical line. This is a + domain-specific setting, overriding :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + +Options for the Javascript domain +--------------------------------- + +.. confval:: javascript_maximum_signature_line_length + + If a signature's length in characters exceeds the number set, each + parameter will be displayed on an individual logical line. This is a + domain-specific setting, overriding :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + Example of configuration file ----------------------------- diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index ac99a28bd11..cbece86e826 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -231,6 +231,16 @@ The following directives are provided for module and class contents: Describe the location where the object is defined. The default value is the module specified by :rst:dir:`py:currentmodule`. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's arguments will be emitted on a single logical + line, overriding :confval:`python_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + + .. rst:directive:: .. py:data:: name Describes global data in a module, including both variables and values used @@ -329,6 +339,15 @@ The following directives are provided for module and class contents: Describe the location where the object is defined. The default value is the module specified by :rst:dir:`py:currentmodule`. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the class constructor's arguments will be emitted on a single + logical line, overriding :confval:`python_maximum_signature_line_length` + and :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. py:attribute:: name Describes an object data attribute. The description should include @@ -441,6 +460,15 @@ The following directives are provided for module and class contents: Describe the location where the object is defined. The default value is the module specified by :rst:dir:`py:currentmodule`. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the method's arguments will be emitted on a single logical + line, overriding :confval:`python_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:option:: staticmethod :type: no value @@ -494,6 +522,15 @@ The following directives are provided for module and class contents: There is no ``py:deco`` role to link to a decorator that is marked up with this directive; rather, use the :rst:role:`py:func` role. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the decorator's arguments will be emitted on a single logical + line, overriding :confval:`python_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. py:decoratormethod:: name .. py:decoratormethod:: name(signature) @@ -763,6 +800,15 @@ The C domain (name **c**) is suited for documentation of C API. :retval NULL: under some conditions. :retval NULL: under some other conditions as well. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's parameters will be emitted on a single logical + line, overriding :confval:`c_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. c:macro:: name .. c:macro:: name(arg list) @@ -776,6 +822,15 @@ The C domain (name **c**) is suited for documentation of C API. .. versionadded:: 3.0 The function style variant. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the macro's parameters will be emitted on a single logical + line, overriding :confval:`c_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. c:struct:: name Describes a C struct. @@ -1126,6 +1181,15 @@ visibility statement (``public``, ``private`` or ``protected``). .. cpp:function:: template<> \ void print(int i) + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's parameters will be emitted on a single logical + line, overriding :confval:`cpp_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. cpp:member:: (member) variable declaration .. cpp:var:: (member) variable declaration @@ -1908,6 +1972,15 @@ The JavaScript domain (name **js**) provides the following directives: :throws SomeError: For whatever reason in that case. :returns: Something. + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's parameters will be emitted on a single logical + line, overriding :confval:`javascript_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. js:method:: name(signature) This directive is an alias for :rst:dir:`js:function`, however it describes @@ -1915,6 +1988,15 @@ The JavaScript domain (name **js**) provides the following directives: .. versionadded:: 1.6 + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's parameters will be emitted on a single logical + line, overriding :confval:`javascript_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. js:class:: name Describes a constructor that creates an object. This is basically like a @@ -1933,6 +2015,15 @@ The JavaScript domain (name **js**) provides the following directives: :param string name: The name of the animal :param number age: an optional age for the animal + .. rst:directive:option:: single-line-parameter-list + :type: no value + + Ensures that the function's parameters will be emitted on a single logical + line, overriding :confval:`javascript_maximum_signature_line_length` and + :confval:`maximum_signature_line_length`. + + .. versionadded:: 7.1 + .. rst:directive:: .. js:data:: name Describes a global variable or constant. diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 44655d9bed1..e92d32a0ef8 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -246,7 +246,12 @@ def astext(self) -> str: 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 ad7c3b56898..a4e66193493 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 c583a770d13..0bb505fba5b 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -727,9 +727,19 @@ def _stringify(self, transform: StringifyTransform) -> str: 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 @@ def run(self) -> list[Node]: 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 a7d16aa06d4..41f2bd07682 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -2142,9 +2142,19 @@ def _stringify(self, transform: StringifyTransform) -> str: 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 @@ def run(self) -> list[Node]: 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 initStuff(app): 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 093e291ca57..c6baab8a9ee 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 @@ def handle_signature(self, sig: str, signode: desc_signature) -> 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 @@ 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) + _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 @@ def get_full_qualified_name(self, node: Element) -> str | None: 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 eef78aa8052..3fda5270351 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -258,10 +258,11 @@ def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: 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 @@ def handle_signature(self, sig: str, signode: desc_signature) -> 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 @@ def handle_signature(self, sig: str, signode: desc_signature) -> 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 b4ff1f9d02d..a2038a9f160 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 913bc8210a6..292facc9132 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 9d5e4419d04..9ae18026720 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 767d558be20..15938cdc54e 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 ab26bab1eb8..e7d932286c5 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -150,14 +150,26 @@ def depart_desc_returns(self, node: Element) -> None: def visit_desc_parameterlist(self, node: Element) -> None: 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([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('\n\n') self.body.append(')') # If required parameters are still to come, then put the comma after @@ -167,28 +179,82 @@ def depart_desc_parameterlist(self, node: Element) -> None: # 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('') def depart_desc_parameter(self, node: Element) -> None: if not node.hasattr('noemph'): self.body.append('') - 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('\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('[') + 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('[') + # 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('[') + self.body.append('\n') + # Else, open a new bracket, append the parameter separator, + # and end the line. + else: + self.body.append('[') + self.body.append(self.param_separator) + self.body.append('\n') + else: + self.body.append('[') def depart_desc_optional(self, node: Element) -> None: self.optional_param_level -= 1 - self.body.append(']') + 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(']') + # 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('\n') + else: + 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(self.starttag(node, 'em', '', CLASS='property')) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index e7d31b70e31..37c73ae5a60 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -702,7 +702,10 @@ def depart_desc(self, node: Element) -> None: 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 @@ def depart_desc_returns(self, node: Element) -> None: 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 3bce03ac6cb..8e3d9df240d 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -594,24 +594,99 @@ def depart_desc_returns(self, node: Element) -> None: 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 diff --git a/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py b/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py new file mode 100644 index 00000000000..ba480ed2884 --- /dev/null +++ b/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py @@ -0,0 +1 @@ +c_maximum_signature_line_length = len("str hello(str name)") - 1 diff --git a/tests/roots/test-domain-c-c_maximum_signature_line_length/index.rst b/tests/roots/test-domain-c-c_maximum_signature_line_length/index.rst new file mode 100644 index 00000000000..be20940ec33 --- /dev/null +++ b/tests/roots/test-domain-c-c_maximum_signature_line_length/index.rst @@ -0,0 +1,4 @@ +domain-c-c_maximum_signature_line_length +======================================== + +.. c:function:: str hello(str name) diff --git a/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py new file mode 100644 index 00000000000..1eb3a64bfc4 --- /dev/null +++ b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py @@ -0,0 +1 @@ +cpp_maximum_signature_line_length = len("str hello(str name)") - 1 diff --git a/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/index.rst b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/index.rst new file mode 100644 index 00000000000..425908cb9b4 --- /dev/null +++ b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/index.rst @@ -0,0 +1,4 @@ +domain-cpp-cpp_maximum_signature_line_length +============================================ + +.. cpp:function:: str hello(str name) diff --git a/tests/roots/test-domain-js-javascript_maximum_signature_line_length/conf.py b/tests/roots/test-domain-js-javascript_maximum_signature_line_length/conf.py new file mode 100644 index 00000000000..d7c9331bd83 --- /dev/null +++ b/tests/roots/test-domain-js-javascript_maximum_signature_line_length/conf.py @@ -0,0 +1 @@ +javascript_maximum_signature_line_length = 1 diff --git a/tests/roots/test-domain-js-javascript_maximum_signature_line_length/index.rst b/tests/roots/test-domain-js-javascript_maximum_signature_line_length/index.rst new file mode 100644 index 00000000000..b79fc1a8fdf --- /dev/null +++ b/tests/roots/test-domain-js-javascript_maximum_signature_line_length/index.rst @@ -0,0 +1,6 @@ +domain-js-maximum_signature_line_length +======================================= + +.. js:function:: hello(name) + +.. js:function:: foo([a, [b, ]]c, d[, e, f]) diff --git a/tests/roots/test-domain-py-python_maximum_signature_line_length/conf.py b/tests/roots/test-domain-py-python_maximum_signature_line_length/conf.py new file mode 100644 index 00000000000..45f620db423 --- /dev/null +++ b/tests/roots/test-domain-py-python_maximum_signature_line_length/conf.py @@ -0,0 +1 @@ +python_maximum_signature_line_length = 1 diff --git a/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst new file mode 100644 index 00000000000..75e46830512 --- /dev/null +++ b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst @@ -0,0 +1,6 @@ +domain-py-maximum_signature_line_length +======================================= + +.. py:function:: hello(name: str) -> str + +.. py:function:: foo([a, [b, ]]c, d[, e, f]) diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 7bf65b794e6..f6c336935d7 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -3,7 +3,7 @@ import os import re import subprocess -from itertools import product +from itertools import chain, product from pathlib import Path from shutil import copyfile from subprocess import CalledProcessError @@ -95,11 +95,18 @@ def skip_if_stylefiles_notfound(testfunc): @skip_if_requested @skip_if_stylefiles_notfound @pytest.mark.parametrize( - "engine,docclass", - product(LATEX_ENGINES, DOCCLASSES), + "engine,docclass,python_maximum_signature_line_length", + # Only running test with `python_maximum_signature_line_length` not None with last + # LaTeX engine to reduce testing time, as if this configuration does not fail with + # one engine, it's almost impossible it would fail with another. + chain( + product(LATEX_ENGINES[:-1], DOCCLASSES, [None]), + product([LATEX_ENGINES[-1]], DOCCLASSES, [1]), + ), ) -@pytest.mark.sphinx('latex') -def test_build_latex_doc(app, status, warning, engine, docclass): +@pytest.mark.sphinx('latex', freshenv=True) +def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_signature_line_length): + app.config.python_maximum_signature_line_length = python_maximum_signature_line_length app.config.intersphinx_mapping = { 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), } @@ -113,7 +120,6 @@ def test_build_latex_doc(app, status, warning, engine, docclass): normalize_intersphinx_mapping(app, app.config) load_mappings(app) app.builder.init() - LaTeXTranslator.ignore_missing_images = True app.builder.build_all() @@ -1734,3 +1740,16 @@ def count_label(name): # ensure that we did not forget any label to check # and if so, report them nicely in case of failure assert sorted(tested_labels) == sorted(output_labels) + + +@pytest.mark.sphinx('latex', testroot='domain-py-python_maximum_signature_line_length', + confoverrides={'python_maximum_signature_line_length': 23}) +def test_one_parameter_per_line(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'python.tex').read_text(encoding='utf8') + + # TODO: should these asserts check presence or absence of a final \sphinxparamcomma? + # signature of 23 characters is too short to trigger one-param-per-line mark-up + assert ('\\pysiglinewithargsret{\\sphinxbfcode{\\sphinxupquote{hello}}}' in result) + + assert ('\\pysigwithonelineperarg{\\sphinxbfcode{\\sphinxupquote{foo}}}' in result) diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 1f894c84a3e..a6316484533 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -7,7 +7,18 @@ import pytest from sphinx import addnodes -from sphinx.addnodes import desc +from sphinx.addnodes import ( + desc, + desc_content, + desc_name, + desc_parameter, + desc_parameterlist, + desc_sig_name, + desc_sig_space, + desc_signature, + desc_signature_line, + pending_xref, +) from sphinx.domains.c import ( DefinitionError, DefinitionParser, @@ -19,6 +30,7 @@ from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.writers.text import STDINDENT class Config: @@ -814,3 +826,251 @@ def test_domain_c_parse_noindexentry(app): assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) assert_node(doctree[0], addnodes.index, entries=[('single', 'f (C function)', 'c.f', '', None)]) assert_node(doctree[2], addnodes.index, entries=[]) + + +@pytest.mark.sphinx('html', confoverrides={ + 'c_maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_c_maximum_signature_line_length_equal(app): + text = ".. c:function:: str hello(str name)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "name"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'c_maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_c_maximum_signature_line_length_force_single(app): + text = (".. c:function:: str hello(str names)\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "names"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'c_maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_c_maximum_signature_line_length_break(app): + text = ".. c:function:: str hello(str names)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "names"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_maximum_signature_line_length_equal(app): + text = ".. c:function:: str hello(str name)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "name"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_maximum_signature_line_length_force_single(app): + text = (".. c:function:: str hello(str names)\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "names"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cfunction_signature_with_maximum_signature_line_length_break(app): + text = ".. c:function:: str hello(str names)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="c", objtype="function", noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, "names"], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'c_maximum_signature_line_length': len('str hello(str name)'), + 'maximum_signature_line_length': 1, +}) +def test_c_maximum_signature_line_length_overrides_global(app): + text = '.. c:function:: str hello(str name)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )] + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='c', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, "str"]], + desc_sig_space, + [desc_sig_name, 'name'], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', testroot='domain-c-c_maximum_signature_line_length') +def test_domain_c_c_maximum_signature_line_length_in_html(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf-8') + expected = """\ + +
+
\ +str\ + \ +name,\ +
+
+ +)\ +\ +
\ + +""" + assert expected in content + + +@pytest.mark.sphinx( + 'text', testroot='domain-c-c_maximum_signature_line_length', +) +def test_domain_c_c_maximum_signature_line_length_in_text(app, status, warning): + app.build() + content = (app.outdir / 'index.txt').read_text(encoding='utf8') + param_line_fmt = STDINDENT * " " + "{}\n" + + expected_parameter_list_hello = "(\n{})".format(param_line_fmt.format("str name,")) + + assert expected_parameter_list_hello in content diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 55542e655d6..49b0c22e9ba 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -8,7 +8,18 @@ import sphinx.domains.cpp as cppDomain from sphinx import addnodes -from sphinx.addnodes import desc +from sphinx.addnodes import ( + desc, + desc_content, + desc_name, + desc_parameter, + desc_parameterlist, + desc_sig_name, + desc_sig_space, + desc_signature, + desc_signature_line, + pending_xref, +) from sphinx.domains.cpp import ( DefinitionError, DefinitionParser, @@ -20,6 +31,7 @@ from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.writers.text import STDINDENT def parse(name, string): @@ -1486,3 +1498,243 @@ def test_domain_cpp_normalize_unspecialized_template_args(make_app, app_params): ) warning = app2._warning.getvalue() assert 'Internal C++ domain error during symbol merging' not in warning + + +@pytest.mark.sphinx('html', confoverrides={ + 'cpp_maximum_signature_line_length': len('str hello(str name)'), +}) +def test_cpp_function_signature_with_cpp_maximum_signature_line_length_equal(app): + text = '.. cpp:function:: str hello(str name)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'name'], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'cpp_maximum_signature_line_length': len('str hello(str name)'), +}) +def test_cpp_function_signature_with_cpp_maximum_signature_line_length_force_single(app): + text = ('.. cpp:function:: str hello(str names)\n' + ' :single-line-parameter-list:') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'names']), + ]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'cpp_maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cpp_function_signature_with_cpp_maximum_signature_line_length_break(app): + text = '.. cpp:function:: str hello(str names)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'names']), + ]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len('str hello(str name)'), +}) +def test_cpp_function_signature_with_maximum_signature_line_length_equal(app): + text = '.. cpp:function:: str hello(str name)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'name'], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len('str hello(str name)'), +}) +def test_cpp_function_signature_with_maximum_signature_line_length_force_single(app): + text = ('.. cpp:function:: str hello(str names)\n' + ' :single-line-parameter-list:') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'names']), + ]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("str hello(str name)"), +}) +def test_cpp_function_signature_with_maximum_signature_line_length_break(app): + text = '.. cpp:function:: str hello(str names)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_signature_line, ( + pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, 'hello']], + desc_parameterlist, + )], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'names']), + ]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'cpp_maximum_signature_line_length': len('str hello(str name)'), + 'maximum_signature_line_length': 1, +}) +def test_cpp_maximum_signature_line_length_overrides_global(app): + text = '.. cpp:function:: str hello(str name)' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ([desc_signature, ([desc_signature_line, (pending_xref, + desc_sig_space, + [desc_name, [desc_sig_name, "hello"]], + desc_parameterlist)])], + desc_content)], + )) + assert_node(doctree[1], addnodes.desc, desctype='function', + domain='cpp', objtype='function', noindex=False) + assert_node(doctree[1][0][0][3], [desc_parameterlist, desc_parameter, ( + [pending_xref, [desc_sig_name, 'str']], + desc_sig_space, + [desc_sig_name, 'name'], + )]) + assert_node(doctree[1][0][0][3], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', testroot='domain-cpp-cpp_maximum_signature_line_length') +def test_domain_cpp_cpp_maximum_signature_line_length_in_html(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf-8') + expected = """\ + +
+
\ +str\ + \ +name,\ +
+
+ +)\ + +
\ +\ +name\ +,\ +
+ + +)\ +
\ +\ +""" + assert expected_parameter_list_hello in content + + param_line_fmt = '
{}
\n' + param_name_fmt = ( + '{}' + ) + optional_fmt = '{}' + + expected_a = param_line_fmt.format( + optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["), + ) + assert expected_a in content + + expected_b = param_line_fmt.format( + param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"), + ) + assert expected_b in content + + expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",") + assert expected_c in content + + expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",") + assert expected_d in content + + expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",") + assert expected_e in content + + expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]")) + assert expected_f in content + + expected_parameter_list_foo = """\ + +
+{}{}{}{}{}{}
+ +)\ +\ +\ +""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f) + assert expected_parameter_list_foo in content + + +@pytest.mark.sphinx( + 'text', testroot='domain-js-javascript_maximum_signature_line_length', +) +def test_domain_js_javascript_maximum_signature_line_length_in_text(app, status, warning): + app.build() + content = (app.outdir / 'index.txt').read_text(encoding='utf8') + param_line_fmt = STDINDENT * " " + "{}\n" + + expected_parameter_list_hello = "(\n{})".format(param_line_fmt.format("name,")) + + assert expected_parameter_list_hello in content + + expected_a = param_line_fmt.format("[a,[") + assert expected_a in content + + expected_b = param_line_fmt.format("b,]]") + assert expected_b in content + + expected_c = param_line_fmt.format("c,") + assert expected_c in content + + expected_d = param_line_fmt.format("d[,") + assert expected_d in content + + expected_e = param_line_fmt.format("e,") + assert expected_e in content + + expected_f = param_line_fmt.format("f,]") + assert expected_f in content + + expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format( + expected_a, expected_b, expected_c, expected_d, expected_e, expected_f, + ) + assert expected_parameter_list_foo in content diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 6cac6cba1df..2b84f01c00d 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -38,6 +38,7 @@ ) from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.writers.text import STDINDENT def parse(sig): @@ -1460,6 +1461,308 @@ def test_signature_line_number(app, include_options): assert line == 1 +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_equal(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_force_single(app): + text = (".. py:function:: hello(names: str) -> str\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_break(app): + text = ".. py:function:: hello(names: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_equal(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_force_single(app): + text = (".. py:function:: hello(names: str) -> str\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_break(app): + text = ".. py:function:: hello(names: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "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, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx( + 'html', + confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), + 'maximum_signature_line_length': 1, + }, +) +def test_python_maximum_signature_line_length_overrides_global(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + expected_doctree = (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"])], + desc_content)]) + assert_node(doctree, expected_doctree) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", noindex=False) + signame_node = [desc_sig_name, "name"] + expected_sig = [desc_parameterlist, desc_parameter, (signame_node, + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"])] + assert_node(doctree[1][0][1], expected_sig) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx( + 'html', testroot='domain-py-python_maximum_signature_line_length', +) +def test_domain_py_python_maximum_signature_line_length_in_html(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + expected_parameter_list_hello = """\ + +
+
\ +\ +name\ +:\ + \ +str\ +,\ +
+
+ +) \ +\ + \ +str\ +\ +\ +\ +""" + assert expected_parameter_list_hello in content + + param_line_fmt = '
{}
\n' + param_name_fmt = ( + '{}' + ) + optional_fmt = '{}' + + expected_a = param_line_fmt.format( + optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["), + ) + assert expected_a in content + + expected_b = param_line_fmt.format( + param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"), + ) + assert expected_b in content + + expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",") + assert expected_c in content + + expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",") + assert expected_d in content + + expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",") + assert expected_e in content + + expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]")) + assert expected_f in content + + expected_parameter_list_foo = """\ + +
+{}{}{}{}{}{}
+ +)\ +\ +\ +""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f) + assert expected_parameter_list_foo in content + + +@pytest.mark.sphinx( + 'text', testroot='domain-py-python_maximum_signature_line_length', +) +def test_domain_py_python_maximum_signature_line_length_in_text(app, status, warning): + app.build() + content = (app.outdir / 'index.txt').read_text(encoding='utf8') + param_line_fmt = STDINDENT * " " + "{}\n" + + expected_parameter_list_hello = "(\n{}) -> str".format(param_line_fmt.format("name: str,")) + + assert expected_parameter_list_hello in content + + expected_a = param_line_fmt.format("[a,[") + assert expected_a in content + + expected_b = param_line_fmt.format("b,]]") + assert expected_b in content + + expected_c = param_line_fmt.format("c,") + assert expected_c in content + + expected_d = param_line_fmt.format("d[,") + assert expected_d in content + + expected_e = param_line_fmt.format("e,") + assert expected_e in content + + expected_f = param_line_fmt.format("f,]") + assert expected_f in content + + expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format( + expected_a, expected_b, expected_c, expected_d, expected_e, expected_f, + ) + assert expected_parameter_list_foo in content + + def test_module_content_line_number(app): text = (".. py:module:: foo\n" + "\n" +