From 8942a1dddf2355928f088d6b631db8658034eaae Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 9 May 2023 19:24:18 +0100 Subject: [PATCH 01/17] Test with Docutils 0.20 --- .github/workflows/main.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2efc1c1844c..2f7b69adcf9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ jobs: docutils: - "0.18" - "0.19" + - "0.20" steps: - uses: actions/checkout@v3 @@ -57,13 +58,7 @@ jobs: env: PYTHONWARNINGS: "" - name: Install Docutils ${{ matrix.docutils }} - run: python -m pip install --upgrade "docutils==${{ matrix.docutils }}.*" - if: "!endsWith(matrix.python, '-dev')" - env: - PYTHONWARNINGS: "error,default:pkg_resources is deprecated:DeprecationWarning::,default:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning::" - - name: Install Docutils ${{ matrix.docutils }} (ignore warnings) - run: python -m pip install --upgrade "docutils==${{ matrix.docutils }}.*" - if: "endsWith(matrix.python, '-dev')" + run: python -m pip install --upgrade "docutils~=${{ matrix.docutils }}.0" env: PYTHONWARNINGS: "" - name: Test with pytest From db546189ce1d2a345f4399367ced6ecdd538be5d Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 9 May 2023 21:00:04 +0100 Subject: [PATCH 02/17] Support Docutils 0.20 (#11411) --- CHANGES | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6cc80e91e9e..631ec3b7b00 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,10 @@ Release 7.0.1 (in development) Dependencies ------------ +* #11411: Support `Docutils 0.20`_. Patch by Adam Turner. + +.. _Docutils 0.20: https://docutils.sourceforge.io/RELEASE-NOTES.html#release-0-20-2023-05-04 + Incompatible changes -------------------- diff --git a/pyproject.toml b/pyproject.toml index 424af4938f7..7b48d4efea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "sphinxcontrib-qthelp", "Jinja2>=3.0", "Pygments>=2.13", - "docutils>=0.18.1,<0.20", + "docutils>=0.18.1,<0.21", "snowballstemmer>=2.0", "babel>=2.9", "alabaster>=0.7,<0.8", From 706f5f9cc83f1d62829bb18ad40bfa5e784e202c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 9 May 2023 22:57:39 +0100 Subject: [PATCH 03/17] Warn on deprecated Python-specific index types (#11412) --- CHANGES | 5 ++ doc/usage/configuration.rst | 5 ++ doc/usage/restructuredtext/directives.rst | 71 ++++++++++++++++++----- sphinx/builders/gettext.py | 5 -- sphinx/domains/python.py | 18 +++--- sphinx/locale/__init__.py | 6 -- sphinx/util/nodes.py | 24 ++++---- tests/roots/test-intl/index_entries.txt | 7 --- tests/roots/test-root/markup.txt | 1 - tests/roots/test-warnings/index.rst | 2 +- tests/test_build_gettext.py | 7 --- tests/test_environment_indexentries.py | 2 +- tests/test_environment_toctree.py | 2 +- tests/test_intl.py | 7 --- 14 files changed, 91 insertions(+), 71 deletions(-) diff --git a/CHANGES b/CHANGES index 631ec3b7b00..cbc9d348c17 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,11 @@ Incompatible changes Deprecated ---------- +* #11412: Emit warnings on using a deprecated Python-specific index entry type + (namely, ``module``, ``keyword``, ``operator``, ``object``, ``exception``, + ``statement``, and ``builtin``) in the :rst:dir:`index` directive, and + set the removal version to Sphinx 9. Patch by Adam Turner. + Features added -------------- diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 11cd2be31dd..23e9ba7ab36 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -337,6 +337,7 @@ General configuration * ``epub.unknown_project_files`` * ``epub.duplicated_toc_entry`` * ``i18n.inconsistent_references`` + * ``index`` * ``image.not_readable`` * ``ref.term`` * ``ref.ref`` @@ -388,6 +389,10 @@ General configuration Added ``i18n.inconsistent_references`` + .. versionadded:: 7.1 + + Added ``index`` warning type. + .. confval:: needs_sphinx If set to a ``major.minor`` version string like ``'1.1'``, Sphinx will diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index c9152d1b18b..7082af04622 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -896,9 +896,10 @@ mainly contained in information units, such as the language reference. .. index:: single: execution; context - module: __main__ - module: sys + pair: module; __main__ + pair: module; sys triple: module; search; path + seealso: scope The execution context --------------------- @@ -916,25 +917,63 @@ mainly contained in information units, such as the language reference. The possible entry types are: single - Creates a single index entry. Can be made a subentry by separating the - subentry text with a semicolon (this notation is also used below to - describe what entries are created). + Creates a single index entry. + Can be made a sub-entry by separating the sub-entry text with a semicolon + (this notation is also used below to describe what entries are created). + Examples: + + .. code:: reStructuredText + + .. index:: single: execution + single: execution; context + + - ``single: execution`` creates an index entry labelled ``execution``. + - ``single: execution; context`` creates an sub-entry of ``execution`` + labelled ``context``. pair - ``pair: loop; statement`` is a shortcut that creates two index entries, - namely ``loop; statement`` and ``statement; loop``. + A shortcut to create two index entries. + The pair of values must be separated by a semicolon. + Example: + + .. code:: reStructuredText + + .. index:: pair: loop; statement + + This would create two index entries; ``loop; statement`` and ``statement; loop``. triple - Likewise, ``triple: module; search; path`` is a shortcut that creates - three index entries, which are ``module; search path``, ``search; path, - module`` and ``path; module search``. + A shortcut to create three index entries. + All three values must be separated by a semicolon. + Example: + + .. code:: reStructuredText + + .. index:: triple: module; search; path + + This would create three index entries; ``module; search path``, + ``search; path, module``, and ``path; module search``. see - ``see: entry; other`` creates an index entry that refers from ``entry`` to - ``other``. + A shortcut to create an index entry that refers to another entry. + Example: + + .. code:: reStructuredText + + .. index:: see: entry; other + + This would create an index entry referring from ``entry`` to ``other`` + (i.e. 'entry': See 'other'). seealso - Like ``see``, but inserts "see also" instead of "see". + Like ``see``, but inserts 'see also' instead of 'see'. module, keyword, operator, object, exception, statement, builtin - These all create two index entries. For example, ``module: hashlib`` - creates the entries ``module; hashlib`` and ``hashlib; module``. (These - are Python-specific and therefore deprecated.) + These **deprecated** shortcuts all create two index entries. + For example, ``module: hashlib`` creates the entries ``module; hashlib`` + and ``hashlib; module``. + + .. deprecated:: 1.0 + These Python-specific entry types are deprecated. + + .. versionchanged:: 7.1 + Removal version set to Sphinx 9.0. + Using these entry types will now emit warnings with the ``index`` category. You can mark up "main" index entries by prefixing them with an exclamation mark. The references to "main" entries are emphasized in the generated diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index 4d460109b30..697b75e4053 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -16,7 +16,6 @@ from sphinx import addnodes, package_dir from sphinx.application import Sphinx from sphinx.builders import Builder -from sphinx.domains.python import pairindextypes from sphinx.errors import ThemeError from sphinx.locale import __ from sphinx.util import logging, split_index_msg @@ -159,10 +158,6 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None: for node, entries in traverse_translatable_index(doctree): for typ, msg, _tid, _main, _key in entries: for m in split_index_msg(typ, msg): - if typ == 'pair' and m in pairindextypes.values(): - # avoid built-in translated message was incorporated - # in 'sphinx.util.nodes.process_index_entry' - continue catalog.add(m, node) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index c461cc311fd..eef78aa8052 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -50,13 +50,13 @@ pairindextypes = { - 'module': _('module'), - 'keyword': _('keyword'), - 'operator': _('operator'), - 'object': _('object'), - 'exception': _('exception'), - 'statement': _('statement'), - 'builtin': _('built-in function'), + 'module': 'module', + 'keyword': 'keyword', + 'operator': 'operator', + 'object': 'object', + 'exception': 'exception', + 'statement': 'statement', + 'builtin': 'built-in function', } @@ -729,7 +729,7 @@ def add_target_and_index(self, name_cls: tuple[str, str], sig: str, text = _('%s() (in module %s)') % (name, modname) self.indexnode['entries'].append(('single', text, node_id, '', None)) else: - text = f'{pairindextypes["builtin"]}; {name}()' + text = f'built-in function; {name}()' self.indexnode['entries'].append(('pair', text, node_id, '', None)) def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str | None: @@ -1058,7 +1058,7 @@ def run(self) -> list[Node]: # the platform and synopsis aren't printed; in fact, they are only # used in the modindex currently ret.append(target) - indextext = f'{pairindextypes["module"]}; {modname}' + indextext = f'module; {modname}' inode = addnodes.index(entries=[('pair', indextext, node_id, '', None)]) ret.append(inode) ret.extend(content_node.children) diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index c92640576ab..8ab90d19120 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -223,9 +223,3 @@ def gettext(message: str) -> str: 'tip': _('Tip'), 'warning': _('Warning'), } - -# Moved to sphinx.directives.other (will be overridden later) -versionlabels: dict[str, str] = {} - -# Moved to sphinx.domains.python (will be overridden later) -pairindextypes: dict[str, str] = {} diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 1b43bd72ed8..3dfdd4f2642 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -365,19 +365,23 @@ def process_index_entry(entry: str, targetid: str, if entry.startswith('!'): main = 'main' entry = entry[1:].lstrip() - for type in pairindextypes: - if entry.startswith(type + ':'): - value = entry[len(type) + 1:].strip() - value = pairindextypes[type] + '; ' + value + for index_type in pairindextypes: + if entry.startswith(f'{index_type}:'): + value = entry[len(index_type) + 1:].strip() + value = f'{pairindextypes[index_type]}; {value}' + # xref RemovedInSphinx90Warning + logger.warning(__('%r is deprecated for index entries (from entry %r). ' + "Use 'pair: %s' instead."), + index_type, entry, value, type='index') indexentries.append(('pair', value, targetid, main, None)) break else: - for type in indextypes: - if entry.startswith(type + ':'): - value = entry[len(type) + 1:].strip() - if type == 'double': - type = 'pair' - indexentries.append((type, value, targetid, main, None)) + for index_type in indextypes: + if entry.startswith(f'{index_type}:'): + value = entry[len(index_type) + 1:].strip() + if index_type == 'double': + index_type = 'pair' + indexentries.append((index_type, value, targetid, main, None)) break # shorthand notation for single entries else: diff --git a/tests/roots/test-intl/index_entries.txt b/tests/roots/test-intl/index_entries.txt index c914a4b4328..e9300d0ef94 100644 --- a/tests/roots/test-intl/index_entries.txt +++ b/tests/roots/test-intl/index_entries.txt @@ -20,12 +20,5 @@ various index entries triple: First; Second; Third see: Entry; Mailing List seealso: See; Newsletter - module: Module - keyword: Keyword - operator: Operator - object: Object - exception: Exception - statement: Statement - builtin: Builtin That's all. diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt index 41a2fa58310..b59a652ee13 100644 --- a/tests/roots/test-root/markup.txt +++ b/tests/roots/test-root/markup.txt @@ -373,7 +373,6 @@ Index markup pair: entry; pair double: entry; double triple: index; entry; triple - keyword: with see: from; to seealso: fromalso; toalso diff --git a/tests/roots/test-warnings/index.rst b/tests/roots/test-warnings/index.rst index 4110e93d04f..8c4f6bb8a16 100644 --- a/tests/roots/test-warnings/index.rst +++ b/tests/roots/test-warnings/index.rst @@ -27,7 +27,7 @@ Don't download :download:`this `. .. index:: single: pair: - keyword: + seealso: .. Invalid code-block .. code-block:: c diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index ebd46275af5..dee9757b024 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -117,13 +117,6 @@ def msgid_getter(msgid): "Third", "Entry", "See", - "Module", - "Keyword", - "Operator", - "Object", - "Exception", - "Statement", - "Builtin", ] for expect in expected_msgids: assert expect in msgids diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index 2fb7faee0e7..4cfdc282336 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -1,4 +1,4 @@ -"""Test the sphinx.environment.managers.indexentries.""" +"""Test the sphinx.environment.adapters.indexentries.""" import pytest diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index 3488fe138f5..9b0047bedd3 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -1,4 +1,4 @@ -"""Test the sphinx.environment.managers.toctree.""" +"""Test the sphinx.environment.adapters.toctree.""" import pytest from docutils import nodes diff --git a/tests/test_intl.py b/tests/test_intl.py index feabe0e62de..54fa113c183 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -727,13 +727,6 @@ def wrap_nest(parenttag, childtag, keyword): wrap('a', 'THIRD, FIRST'), wrap_nest('li', 'ul', 'ENTRY'), wrap_nest('li', 'ul', 'SEE'), - wrap('a', 'MODULE'), - wrap('a', 'KEYWORD'), - wrap('a', 'OPERATOR'), - wrap('a', 'OBJECT'), - wrap('a', 'EXCEPTION'), - wrap('a', 'STATEMENT'), - wrap('a', 'BUILTIN'), ] for expr in expected_exprs: assert_re_search(expr, result, re.M) From ae206694e68bea074aca633ea0d32e9ed882a95f Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 11 May 2023 05:19:31 +0100 Subject: [PATCH 04/17] html builder: Append CRC32 checksum to asset URIs (#11415) --- CHANGES | 3 +++ sphinx/builders/__init__.py | 7 ++++++ sphinx/builders/html/__init__.py | 41 +++++++++++++++++++++++++------ sphinx/builders/latex/__init__.py | 11 ++++++--- sphinx/builders/texinfo.py | 3 ++- sphinx/ext/graphviz.py | 16 ++++++------ tests/test_build_html.py | 35 +++++++++++++++++--------- tests/test_theming.py | 4 +-- 8 files changed, 86 insertions(+), 34 deletions(-) diff --git a/CHANGES b/CHANGES index cbc9d348c17..7adb7cad583 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,9 @@ Deprecated Features added -------------- +* #11415: Add a checksum to JavaScript and CSS asset URIs included within + generated HTML, using the CRC32 algorithm. + Bugs fixed ---------- diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 852f252596e..a5d5a1ef69b 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -560,6 +560,9 @@ def write( with progress_message(__('preparing documents')): self.prepare_writing(docnames) + with progress_message(__('copying assets')): + self.copy_assets() + if self.parallel_ok: # number of subprocesses is parallel-1 because the main process # is busy loading doctrees and doing write_doc_serialized() @@ -620,6 +623,10 @@ def prepare_writing(self, docnames: set[str]) -> None: """A place where you can add logic before :meth:`write_doc` is run""" raise NotImplementedError + def copy_assets(self) -> None: + """Where assets (images, static files, etc) are copied before writing""" + pass + def write_doc(self, docname: str, doctree: nodes.document) -> None: """Where you actually write something to the filesystem.""" raise NotImplementedError diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 8b8c426a4b0..64a5c1f604d 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -8,6 +8,7 @@ import re import sys import warnings +import zlib from datetime import datetime from os import path from typing import IO, Any, Iterable, Iterator, List, Tuple, Type @@ -649,6 +650,12 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A 'page_source_suffix': source_suffix, } + def copy_assets(self) -> None: + self.finish_tasks.add_task(self.copy_download_files) + self.finish_tasks.add_task(self.copy_static_files) + self.finish_tasks.add_task(self.copy_extra_files) + self.finish_tasks.join() + def write_doc(self, docname: str, doctree: nodes.document) -> None: destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings @@ -678,9 +685,6 @@ def finish(self) -> None: self.finish_tasks.add_task(self.gen_pages_from_extensions) self.finish_tasks.add_task(self.gen_additional_pages) self.finish_tasks.add_task(self.copy_image_files) - self.finish_tasks.add_task(self.copy_download_files) - self.finish_tasks.add_task(self.copy_static_files) - self.finish_tasks.add_task(self.copy_extra_files) self.finish_tasks.add_task(self.write_buildinfo) # dump the search index @@ -1193,8 +1197,11 @@ def css_tag(css: Stylesheet) -> str: value = css.attributes[key] if value is not None: attrs.append(f'{key}="{html.escape(value, True)}"') - attrs.append('href="%s"' % pathto(css.filename, resource=True)) - return '' % ' '.join(attrs) + uri = pathto(css.filename, resource=True) + if checksum := _file_checksum(app.outdir, css.filename): + uri += f'?v={checksum}' + attrs.append(f'href="{uri}"') + return f'' context['css_tag'] = css_tag @@ -1217,14 +1224,17 @@ def js_tag(js: JavaScript) -> str: if key == 'body': body = value elif key == 'data_url_root': - attrs.append('data-url_root="%s"' % pathto('', resource=True)) + attrs.append(f'data-url_root="{pathto("", resource=True)}"') else: attrs.append(f'{key}="{html.escape(value, True)}"') if js.filename: - attrs.append('src="%s"' % pathto(js.filename, resource=True)) + uri = pathto(js.filename, resource=True) + if checksum := _file_checksum(app.outdir, js.filename): + uri += f'?v={checksum}' + attrs.append(f'src="{uri}"') else: # str value (old styled) - attrs.append('src="%s"' % pathto(js, resource=True)) + attrs.append(f'src="{pathto(js, resource=True)}"') if attrs: return f'' @@ -1234,6 +1244,21 @@ def js_tag(js: JavaScript) -> str: context['js_tag'] = js_tag +def _file_checksum(outdir: str, filename: str) -> str: + # Don't generate checksums for HTTP URIs + if '://' in filename: + return '' + try: + # Ensure universal newline mode is used to avoid checksum differences + with open(path.join(outdir, filename), encoding='utf-8') as f: + content = f.read().encode(encoding='utf-8') + except FileNotFoundError: + return '' + if not content: + return '' + return f'{zlib.crc32(content):08x}' + + def setup_resource_paths(app: Sphinx, pagename: str, templatename: str, context: dict, doctree: Node) -> None: """Set up relative resource paths.""" diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py index 335518f2308..e3e4d5042f2 100644 --- a/sphinx/builders/latex/__init__.py +++ b/sphinx/builders/latex/__init__.py @@ -254,6 +254,12 @@ def write_stylesheet(self) -> None: f.write('% Its contents depend on pygments_style configuration variable.\n\n') f.write(highlighter.get_stylesheet()) + def copy_assets(self) -> None: + self.copy_support_files() + + if self.config.latex_additional_files: + self.copy_latex_additional_files() + def write(self, *ignored: Any) -> None: docwriter = LaTeXWriter(self) with warnings.catch_warnings(): @@ -267,6 +273,7 @@ def write(self, *ignored: Any) -> None: self.init_document_data() self.write_stylesheet() + self.copy_assets() for entry in self.document_data: docname, targetname, title, author, themename = entry[:5] @@ -371,10 +378,6 @@ def assemble_doctree( def finish(self) -> None: self.copy_image_files() self.write_message_catalog() - self.copy_support_files() - - if self.config.latex_additional_files: - self.copy_latex_additional_files() @progress_message(__('copying TeX support files')) def copy_support_files(self) -> None: diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index 078991d9dcd..0b642af4cae 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -85,6 +85,7 @@ def init_document_data(self) -> None: def write(self, *ignored: Any) -> None: self.init_document_data() + self.copy_assets() for entry in self.document_data: docname, targetname, title, author = entry[:4] targetname += '.texi' @@ -168,7 +169,7 @@ def assemble_doctree( pendingnode.replace_self(newnodes) return largetree - def finish(self) -> None: + def copy_assets(self) -> None: self.copy_support_files() def copy_image_files(self, targetname: str) -> None: diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index c0a99be08c6..37626e04f4c 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -8,7 +8,7 @@ import subprocess from os import path from subprocess import CalledProcessError -from typing import Any +from typing import TYPE_CHECKING, Any from docutils import nodes from docutils.nodes import Node @@ -20,7 +20,6 @@ from sphinx.locale import _, __ from sphinx.util import logging, sha1 from sphinx.util.docutils import SphinxDirective, SphinxTranslator -from sphinx.util.fileutil import copy_asset from sphinx.util.i18n import search_image_for_language from sphinx.util.nodes import set_source_info from sphinx.util.osutil import ensuredir @@ -31,6 +30,9 @@ from sphinx.writers.texinfo import TexinfoTranslator from sphinx.writers.text import TextTranslator +if TYPE_CHECKING: + from sphinx.config import Config + logger = logging.getLogger(__name__) @@ -391,11 +393,9 @@ def man_visit_graphviz(self: ManualPageTranslator, node: graphviz) -> None: raise nodes.SkipNode -def on_build_finished(app: Sphinx, exc: Exception) -> None: - if exc is None and app.builder.format == 'html': - src = path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css') - dst = path.join(app.outdir, '_static') - copy_asset(src, dst) +def on_config_inited(_app: Sphinx, config: Config) -> None: + css_path = path.join(sphinx.package_dir, 'templates', 'graphviz', 'graphviz.css') + config.html_static_path.append(css_path) def setup(app: Sphinx) -> dict[str, Any]: @@ -412,5 +412,5 @@ def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value('graphviz_dot_args', [], 'html') app.add_config_value('graphviz_output_format', 'png', 'html') app.add_css_file('graphviz.css') - app.connect('build-finished', on_build_finished) + app.connect('config-inited', on_config_inited) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 8591897fb56..c6a36209892 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -1186,19 +1186,32 @@ def test_assets_order(app): content = (app.outdir / 'index.html').read_text(encoding='utf8') # css_files - expected = ['_static/early.css', '_static/pygments.css', '_static/alabaster.css', - 'https://example.com/custom.css', '_static/normal.css', '_static/late.css', - '_static/css/style.css', '_static/lazy.css'] - pattern = '.*'.join('href="%s"' % f for f in expected) - assert re.search(pattern, content, re.S) + expected = [ + '_static/early.css', + '_static/pygments.css?v=b3523f8e', + '_static/alabaster.css?v=039e1c02', + 'https://example.com/custom.css', + '_static/normal.css', + '_static/late.css', + '_static/css/style.css', + '_static/lazy.css', + ] + pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected) + assert re.search(pattern, content, re.DOTALL), content # js_files - expected = ['_static/early.js', - '_static/doctools.js', '_static/sphinx_highlight.js', - 'https://example.com/script.js', '_static/normal.js', - '_static/late.js', '_static/js/custom.js', '_static/lazy.js'] - pattern = '.*'.join('src="%s"' % f for f in expected) - assert re.search(pattern, content, re.S) + expected = [ + '_static/early.js', + '_static/doctools.js?v=888ff710', + '_static/sphinx_highlight.js?v=4825356b', + 'https://example.com/script.js', + '_static/normal.js', + '_static/late.js', + '_static/js/custom.js', + '_static/lazy.js', + ] + pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected) + assert re.search(pattern, content, re.DOTALL), content @pytest.mark.sphinx('html', testroot='html_assets') diff --git a/tests/test_theming.py b/tests/test_theming.py index 57928998804..b7ccda95c98 100644 --- a/tests/test_theming.py +++ b/tests/test_theming.py @@ -99,10 +99,10 @@ def test_dark_style(app, status, warning): assert (app.outdir / '_static' / 'pygments_dark.css').exists() result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '' in result + assert '' in result assert ('') in result + 'href="_static/pygments_dark.css?v=e15ddae3" />') in result @pytest.mark.sphinx(testroot='theming') From c73628dfcac844f89198ccd805e8e35609b37636 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 11 May 2023 13:34:09 +0100 Subject: [PATCH 05/17] Accept a version tuple in ``app.require_sphinx()`` --- CHANGES | 2 ++ sphinx/application.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 7adb7cad583..9ac823cdb4c 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,8 @@ Features added * #11415: Add a checksum to JavaScript and CSS asset URIs included within generated HTML, using the CRC32 algorithm. +* :meth:`~sphinx.application.Sphinx.require_sphinx` now allows the version + requirement to be specified as ``(major, minor)``. Bugs fixed ---------- diff --git a/sphinx/application.py b/sphinx/application.py index 67b32269303..495e905704d 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -401,18 +401,26 @@ def setup_extension(self, extname: str) -> None: logger.debug('[app] setting up extension: %r', extname) self.registry.load_extension(self, extname) - def require_sphinx(self, version: str) -> None: + @staticmethod + def require_sphinx(version: tuple[int, int] | str) -> None: """Check the Sphinx version if requested. Compare *version* with the version of the running Sphinx, and abort the build when it is too old. - :param version: The required version in the form of ``major.minor``. + :param version: The required version in the form of ``major.minor`` or + ``(major, minor)``. .. versionadded:: 1.0 + .. versionchanged:: 7.1 + Type of *version* now allows ``(major, minor)`` form. """ - if version > sphinx.__display_version__[:3]: - raise VersionRequirementError(version) + if isinstance(version, tuple): + major, minor = version + else: + major, minor = map(int, version.split('.')[:2]) + if (major, minor) > sphinx.version_info[:2]: + raise VersionRequirementError(f'{major}.{minor}') # event interface def connect(self, event: str, callback: Callable, priority: int = 500) -> int: From 86b07d4a97a225e79150d14e25a768ebc4c087cc Mon Sep 17 00:00:00 2001 From: TLouf <31036680+TLouf@users.noreply.github.com> Date: Thu, 11 May 2023 15:28:57 +0200 Subject: [PATCH 06/17] Allow multi-line object description signatures (#11011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGES | 7 + doc/latex.rst | 6 + doc/usage/configuration.rst | 54 ++++ doc/usage/restructuredtext/domains.rst | 91 ++++++ sphinx/addnodes.py | 7 +- sphinx/config.py | 2 +- sphinx/domains/c.py | 22 +- sphinx/domains/cpp.py | 22 +- sphinx/domains/javascript.py | 17 +- sphinx/domains/python.py | 27 +- sphinx/texinputs/sphinxlatexobjects.sty | 21 ++ sphinx/texinputs/sphinxlatexstyletext.sty | 8 +- sphinx/themes/basic/static/basic.css_t | 10 + sphinx/themes/epub/static/epub.css_t | 10 + sphinx/writers/html5.py | 84 ++++- sphinx/writers/latex.py | 68 +++- sphinx/writers/text.py | 87 ++++- .../conf.py | 1 + .../index.rst | 4 + .../conf.py | 1 + .../index.rst | 4 + .../conf.py | 1 + .../index.rst | 6 + .../conf.py | 1 + .../index.rst | 6 + tests/test_build_latex.py | 31 +- tests/test_domain_c.py | 262 ++++++++++++++- tests/test_domain_cpp.py | 254 ++++++++++++++- tests/test_domain_js.py | 261 +++++++++++++++ tests/test_domain_py.py | 303 ++++++++++++++++++ 30 files changed, 1635 insertions(+), 43 deletions(-) create mode 100644 tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py create mode 100644 tests/roots/test-domain-c-c_maximum_signature_line_length/index.rst create mode 100644 tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py create mode 100644 tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/index.rst create mode 100644 tests/roots/test-domain-js-javascript_maximum_signature_line_length/conf.py create mode 100644 tests/roots/test-domain-js-javascript_maximum_signature_line_length/index.rst create mode 100644 tests/roots/test-domain-py-python_maximum_signature_line_length/conf.py create mode 100644 tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst 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" + From e09d02e440691e842377f0ebea7910d307509a81 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Thu, 11 May 2023 17:43:48 -0400 Subject: [PATCH 07/17] Allow ``copyright`` to contain multiple entries (#10983) Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- CHANGES | 2 ++ doc/_themes/sphinx13/layout.html | 2 +- doc/usage/configuration.rst | 4 +++ sphinx/config.py | 4 +-- sphinx/themes/basic/layout.html | 26 +++++++++++++++---- tests/roots/test-copyright-multiline/conf.py | 8 ++++++ .../roots/test-copyright-multiline/index.rst | 3 +++ tests/test_config.py | 22 ++++++++++++++++ 8 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 tests/roots/test-copyright-multiline/conf.py create mode 100644 tests/roots/test-copyright-multiline/index.rst diff --git a/CHANGES b/CHANGES index 07bc8157ef1..bbea8709bcf 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,8 @@ Features added 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. +* #10983: Support for multiline copyright statements in the footer block. + Patch by Stefanie Molin Bugs fixed ---------- diff --git a/doc/_themes/sphinx13/layout.html b/doc/_themes/sphinx13/layout.html index 8010517a69a..86a794306b9 100644 --- a/doc/_themes/sphinx13/layout.html +++ b/doc/_themes/sphinx13/layout.html @@ -54,7 +54,7 @@

{{ _('Site navigation') }}

{%- block footer %} {%- endblock %} diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 1fc4c674b4c..133e2099df9 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -73,6 +73,10 @@ Project information A copyright statement in the style ``'2008, Author Name'``. + .. versionchanged:: 7.1 + The value may now be a sequence of copyright statements in the above form, + which will be displayed each to their own line. + .. confval:: project_copyright An alias of :confval:`copyright`. diff --git a/sphinx/config.py b/sphinx/config.py index a4e66193493..b8cf1eda2ca 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -89,8 +89,8 @@ class Config: # general options 'project': ('Python', 'env', []), 'author': ('unknown', 'env', []), - 'project_copyright': ('', 'html', [str]), - 'copyright': (lambda c: c.project_copyright, 'html', [str]), + 'project_copyright': ('', 'html', [str, tuple, list]), + 'copyright': (lambda c: c.project_copyright, 'html', [str, tuple, list]), 'version': ('', 'env', []), 'release': ('', 'env', []), 'today': ('', 'env', []), diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index f3088f79a95..79eb02de8ec 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -183,14 +183,30 @@

{{ _('Navigation') }}

{%- block relbar2 %}{{ relbar() }}{% endblock %} +{%- macro copyright_block() %} + {%- if hasdoc('copyright') %} + {%- set copyright_prefix = '' + _('Copyright') + '' -%} + {%- else %} + {%- set copyright_prefix = _('Copyright') %} + {%- endif %} + {%- if copyright is iterable and copyright is not string %} + {% for copyright_line in copyright %} + {% trans trimmed copyright_prefix=copyright_prefix, copyright=copyright_line|e %} + © {{ copyright_prefix }} {{ copyright }}. + {% endtrans %} + {%- if not loop.last %}
{%- endif %} + {% endfor %} + {%- else %} + {% trans trimmed copyright_prefix=copyright_prefix, copyright=copyright|e %} + © {{ copyright_prefix }} {{ copyright }}. + {% endtrans %} + {%- endif %} +{%- endmacro %} + {%- block footer %}