diff --git a/.flake8 b/.flake8 index 6c182a22c19..e14b09a0543 100644 --- a/.flake8 +++ b/.flake8 @@ -33,5 +33,6 @@ exclude = doc/usage/extensions/example*.py, per-file-ignores = doc/conf.py:W605 + sphinx/events.py:E704, tests/test_extensions/ext_napoleon_pep526_data_google.py:MLL001, tests/test_extensions/ext_napoleon_pep526_data_numpy.py:MLL001, diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml index 7fa7b96f3f9..5c1736d3c5b 100644 --- a/.github/workflows/builddoc.yml +++ b/.github/workflows/builddoc.yml @@ -44,4 +44,3 @@ jobs: --jobs=auto --show-traceback --fail-on-warning - --keep-going diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fd59f7ff119..e20833c7a9f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,12 +24,17 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Get Ruff version from pyproject.toml + run: | + RUFF_VERSION=$(awk -F'[="]' '/\[project\.optional-dependencies\]/ {p=1} /ruff/ {if (p) print $4}' pyproject.toml) + echo "RUFF_VERSION=$RUFF_VERSION" >> $GITHUB_ENV + - name: Install Ruff run: > - RUFF_VERSION=$(awk -F'[="]' '/\[project\.optional-dependencies\]/ {p=1} /ruff/ {if (p) print $4}' pyproject.toml) curl --no-progress-meter --location --fail --proto '=https' --tlsv1.2 - "https://astral.sh/ruff/${RUFF_VERSION}/install.sh" + --write-out "%{stderr}Downloaded: %{url}\n" + "https://astral.sh/ruff/$RUFF_VERSION/install.sh" | sh - name: Lint with Ruff diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fece8861873..29359f90958 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -23,11 +23,14 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +env: + FORCE_COLOR: "1" + jobs: build: runs-on: ubuntu-latest env: - node-version: "16" + node-version: "20" steps: - uses: actions/checkout@v4 @@ -37,5 +40,4 @@ jobs: node-version: ${{ env.node-version }} cache: "npm" - run: npm install - - name: Run headless test - run: xvfb-run -a npm test + - run: npm test diff --git a/CHANGES.rst b/CHANGES.rst index f03f803ea18..61a79bf9966 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,14 @@ Features added output files. * #12474: Support type-dependent search result highlighting via CSS. Patch by Tim Hoffmann. +* #12743: No longer exit on the first warning when + :option:`--fail-on-warning ` is used. + Instead, exit with a non-zero status if any warnings were generated + during the build. + Patch by Adam Turner. +* #12743: Add :option:`sphinx-build --exception-on-warning`, + to raise an exception when warnings are emitted during the build. + Patch by Adam Turner and Jeremy Maitin-Shepard. Bugs fixed ---------- diff --git a/doc/conf.py b/doc/conf.py index fc86fcd6d98..aad73e59bca 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -186,12 +186,15 @@ ('js:func', 'number'), ('js:func', 'string'), ('py:attr', 'srcline'), + ('py:class', '_ConfigRebuild'), # sphinx.application.Sphinx.add_config_value ('py:class', '_StrPath'), # sphinx.environment.BuildEnvironment.doc2path ('py:class', 'Element'), # sphinx.domains.Domain ('py:class', 'Documenter'), # sphinx.application.Sphinx.add_autodocumenter ('py:class', 'IndexEntry'), # sphinx.domains.IndexEntry + ('py:class', 'Lexer'), # sphinx.application.Sphinx.add_lexer ('py:class', 'Node'), # sphinx.domains.Domain ('py:class', 'NullTranslations'), # gettext.NullTranslations + ('py:class', 'Path'), # sphinx.application.Sphinx.connect ('py:class', 'RoleFunction'), # sphinx.domains.Domain ('py:class', 'RSTState'), # sphinx.utils.parsing.nested_parse_to_nodes ('py:class', 'Theme'), # sphinx.application.TemplateBridge @@ -199,6 +202,8 @@ ('py:class', 'StringList'), # sphinx.utils.parsing.nested_parse_to_nodes ('py:class', 'system_message'), # sphinx.utils.docutils.SphinxDirective ('py:class', 'TitleGetter'), # sphinx.domains.Domain + ('py:class', 'todo_node'), # sphinx.application.Sphinx.connect + ('py:class', 'Transform'), # sphinx.application.Sphinx.add_transform ('py:class', 'XRefRole'), # sphinx.domains.Domain ('py:class', 'docutils.nodes.Element'), ('py:class', 'docutils.nodes.Node'), @@ -210,6 +215,7 @@ ('py:class', 'docutils.parsers.rst.states.Inliner'), ('py:class', 'docutils.transforms.Transform'), ('py:class', 'nodes.NodeVisitor'), + ('py:class', 'nodes.TextElement'), # sphinx.application.Sphinx.connect ('py:class', 'nodes.document'), ('py:class', 'nodes.reference'), ('py:class', 'pygments.lexer.Lexer'), diff --git a/doc/extdev/event_callbacks.rst b/doc/extdev/event_callbacks.rst index 1edade48faa..04eae51be1d 100644 --- a/doc/extdev/event_callbacks.rst +++ b/doc/extdev/event_callbacks.rst @@ -107,10 +107,10 @@ Here is a more detailed list of these events. :param app: :class:`.Sphinx` :param env: :class:`.BuildEnvironment` - :param added: ``set[str]`` - :param changed: ``set[str]`` - :param removed: ``set[str]`` - :returns: ``list[str]`` of additional docnames to re-read + :param added: ``Set[str]`` + :param changed: ``Set[str]`` + :param removed: ``Set[str]`` + :returns: ``Sequence[str]`` of additional docnames to re-read Emitted when the environment determines which source files have changed and should be re-read. diff --git a/doc/internals/contributing.rst b/doc/internals/contributing.rst index 0f387341658..0407a1a36b5 100644 --- a/doc/internals/contributing.rst +++ b/doc/internals/contributing.rst @@ -263,7 +263,7 @@ To build the documentation, run the following command: .. code-block:: shell - sphinx-build -M html ./doc ./build/sphinx --fail-on-warning --keep-going + sphinx-build -M html ./doc ./build/sphinx --fail-on-warning This will parse the Sphinx documentation's source files and generate HTML for you to preview in :file:`build/sphinx/html`. diff --git a/doc/man/sphinx-build.rst b/doc/man/sphinx-build.rst index a8a21f8ce93..a9e3a09cbfd 100644 --- a/doc/man/sphinx-build.rst +++ b/doc/man/sphinx-build.rst @@ -43,7 +43,7 @@ Options the source and output directories, before any other options are passed. For example:: - sphinx-build -M html ./source ./build --fail-on-warning --keep-going + sphinx-build -M html ./source ./build --fail-on-warning The *make-mode* provides the same build functionality as a default :ref:`Makefile or Make.bat `, @@ -253,20 +253,35 @@ Options .. option:: -W, --fail-on-warning - Turn warnings into errors. This means that the build stops at the first - warning and ``sphinx-build`` exits with exit status 1. + Turn warnings into errors. + This means that :program:`sphinx-build` exits with exit status 1 + if any warnings are generated during the build. .. versionchanged:: 7.3 Add ``--fail-on-warning`` long option. + .. versionchanged:: 8.1 + :program:`sphinx-build` no longer exits on the first warning, + but instead runs the entire build and exits with exit status 1 + if any warnings were generated. + This behaviour was previously enabled with :option:`--keep-going`. .. option:: --keep-going - Only applicable whilst using :option:`--fail-on-warning`, - which by default exits :program:`sphinx-build` on the first warning. + From Sphinx 8.1, :option:`!--keep-going` is always enabled. + Previously, it was only applicable whilst using :option:`--fail-on-warning`, + which by default exited :program:`sphinx-build` on the first warning. Using :option:`!--keep-going` runs :program:`!sphinx-build` to completion and exits with exit status 1 if errors are encountered. .. versionadded:: 1.8 + .. versionchanged:: 8.1 + :program:`sphinx-build` no longer exits on the first warning, + meaning that in effect :option:`!--fail-on-warning` is always enabled. + The option is retained for compatibility, but may be removed at some + later date. + + .. xref RemovedInSphinx10Warning: deprecate this option in Sphinx 10 + or no earlier than 2026-01-01. .. option:: -T, --show-traceback @@ -287,6 +302,13 @@ Options .. versionchanged:: 7.3 Add ``--pdb`` long option. +.. option:: --exception-on-warning + + Raise an exception when a warning is emitted during the build. + This can be useful in combination with :option:`--pdb` to debug warnings. + + .. versionadded:: 8.1 + .. option:: -h, --help, --version Display usage summary or Sphinx version. diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 7495d3a9681..289218f8093 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -770,6 +770,10 @@ There are also config values that you can set: If ``False`` is given, autodoc forcedly suppresses the error if the imported module emits warnings. By default, ``True``. + .. versionchanged:: 8.1 + This option now has no effect as :option:`!--fail-on-warning` + no longer exits early. + .. confval:: autodoc_inherit_docstrings This value controls the docstrings inheritance. diff --git a/doc/usage/restructuredtext/directives.rst b/doc/usage/restructuredtext/directives.rst index c32d7a1c4e4..de86d4d7989 100644 --- a/doc/usage/restructuredtext/directives.rst +++ b/doc/usage/restructuredtext/directives.rst @@ -117,7 +117,7 @@ The ``toctree`` directive is the central element. .. versionchanged:: 0.6 Added support for external links and "self" references. - .. rubric:: options + .. rubric:: Options .. rst:directive:option:: class: class names :type: a list of class names, separated by spaces @@ -562,6 +562,27 @@ Presentational A rubric is like an informal heading that doesn't correspond to the document's structure, i.e. it does not create a table of contents node. + .. note:: + + If the *title* of the rubric is "Footnotes" (or the selected language's + equivalent), this rubric is ignored by the LaTeX writer, since it is + assumed to only contain footnote definitions and therefore would create an + empty heading. + + .. rubric:: Options + + .. rst:directive:option:: class: class names + :type: a list of class names, separated by spaces + + Assign `class attributes`_. + This is a :dudir:`common option `. + + .. rst:directive:option:: name: label + :type: text + + An implicit target name that can be referenced using :rst:role:`ref`. + This is a :dudir:`common option `. + .. rst:directive:option:: heading-level: n :type: number from 1 to 6 @@ -572,12 +593,6 @@ Presentational or as the corresponding non-numbered sectioning command for LaTeX (see :confval:`latex_toplevel_sectioning`). - .. note:: - - If the *title* of the rubric is "Footnotes" (or the selected language's - equivalent), this rubric is ignored by the LaTeX writer, since it is - assumed to only contain footnote definitions and therefore would create an - empty heading. .. rst:directive:: centered @@ -594,17 +609,22 @@ Presentational compact list by either distributing more than one item horizontally, or reducing spacing between items, depending on the builder. - For builders that support the horizontal distribution, there is a ``columns`` - option that specifies the number of columns; it defaults to 2. Example:: + .. rubric:: Options + + .. rst:directive:option:: columns: n + :type: int + + The number of columns; defaults to 2. + For example:: - .. hlist:: - :columns: 3 + .. hlist:: + :columns: 3 - * A list of - * short items - * that should be - * displayed - * horizontally + * A list of + * short items + * that should be + * displayed + * horizontally .. versionadded:: 0.6 @@ -678,7 +698,7 @@ __ https://pygments.org/docs/lexers As discussed previously, *language* can be any lexer alias supported by Pygments. - .. rubric:: options + .. rubric:: Options .. rst:directive:option:: linenothreshold: threshold :type: number (optional) @@ -723,7 +743,7 @@ __ https://pygments.org/docs/lexers .. versionchanged:: 2.0 The ``language`` argument becomes optional. - .. rubric:: options + .. rubric:: Options .. rst:directive:option:: linenos :type: no value @@ -1050,9 +1070,6 @@ Glossary text only have "term" part. In this case, translated "localized term" will be categorized in "key" group. - .. versionadded:: 0.6 - You can now give the glossary directive a ``:sorted:`` flag that will - automatically sort the entries alphabetically. .. versionchanged:: 1.1 Now supports multiple terms and inline markup in terms. @@ -1060,9 +1077,19 @@ Glossary .. versionchanged:: 1.4 Index key for glossary term should be considered *experimental*. - .. versionchanged:: 4.4 - In internationalized documentation, the ``:sorted:`` flag sorts - according to translated terms. + + .. rubric:: Options + + .. rst:directive:option:: sorted + + Sort the entries alphabetically. + + .. versionadded:: 0.6 + + .. versionchanged:: 4.4 + In internationalized documentation, sort according to translated terms. + + Meta-information markup ----------------------- @@ -1211,7 +1238,7 @@ mainly contained in information units, such as the language reference. .. versionchanged:: 1.1 Added ``see`` and ``seealso`` types, as well as marking main entries. - .. rubric:: options + .. rubric:: Options .. rst:directive:option:: name: a label for hyperlink :type: text @@ -1415,23 +1442,45 @@ or use Python raw strings (``r"raw"``). .. math:: (a + b)^2 = a^2 + 2ab + b^2 - Normally, equations are not numbered. If you want your equation to get a - number, use the ``label`` option. When given, it selects an internal label - for the equation, by which it can be cross-referenced, and causes an equation - number to be issued. See :rst:role:`eq` for an example. The numbering - style depends on the output format. + .. rubric:: Options - There is also an option ``nowrap`` that prevents any wrapping of the given - math in a math environment. When you give this option, you must make sure - yourself that the math is properly set up. For example:: + .. rst:directive:option:: class: class names + :type: a list of class names, separated by spaces - .. math:: - :nowrap: + Assign `class attributes`_. + This is a :dudir:`common option `. + + .. _class attributes: https://docutils.sourceforge.io/docs/ref/doctree.html#classes + + .. rst:directive:option:: name: label + :type: text + + An implicit target name that can be referenced using :rst:role:`ref`. + This is a :dudir:`common option `. + + .. rst:directive:option:: label: label + :type: text + + Normally, equations are not numbered. If you want your equation to get a + number, use the ``label`` option. When given, it selects an internal label + for the equation, by which it can be cross-referenced, and causes an equation + number to be issued. See :rst:role:`eq` for an example. The numbering + style depends on the output format. + + .. rst:directive:option:: nowrap + + Prevent wrapping of the given math in a math environment. + When you give this option, you must make sure + yourself that the math is properly set up. + For example:: + + .. math:: + :nowrap: - \begin{eqnarray} - y & = & ax^2 + bx + c \\ - f(x) & = & x^2 + 2xy + y^2 - \end{eqnarray} + \begin{eqnarray} + y & = & ax^2 + bx + c \\ + f(x) & = & x^2 + 2xy + y^2 + \end{eqnarray} .. _AmSMath LaTeX package: https://www.ams.org/publications/authors/tex/amslatex diff --git a/pyproject.toml b/pyproject.toml index ff26246c2a9..dcca55027f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,7 @@ docs = [ ] lint = [ "flake8>=6.0", - "ruff==0.5.7", + "ruff==0.6.1", "mypy==1.11.1", "sphinx-lint>=0.9", "types-colorama==0.4.15.20240311", diff --git a/sphinx/application.py b/sphinx/application.py index a1589fb230c..ea1f79ba74e 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -10,15 +10,11 @@ import pickle import sys from collections import deque -from collections.abc import Callable, Collection, Sequence # NoQA: TCH003 from io import StringIO from os import path -from typing import IO, TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, overload -from docutils.nodes import TextElement # NoQA: TCH002 from docutils.parsers.rst import Directive, roles -from docutils.transforms import Transform # NoQA: TCH002 -from pygments.lexer import Lexer # NoQA: TCH002 import sphinx from sphinx import locale, package_dir @@ -41,14 +37,22 @@ from sphinx.util.tags import Tags if TYPE_CHECKING: + from collections.abc import Callable, Collection, Iterable, Sequence, Set + from pathlib import Path + from typing import IO, Any, Final, Literal + from docutils import nodes from docutils.nodes import Element, Node from docutils.parsers import Parser + from docutils.transforms import Transform + from pygments.lexer import Lexer + from sphinx import addnodes from sphinx.builders import Builder from sphinx.domains import Domain, Index from sphinx.environment.collectors import EnvironmentCollector from sphinx.ext.autodoc import Documenter + from sphinx.ext.todo import todo_node from sphinx.extension import Extension from sphinx.roles import XRefRole from sphinx.search import SearchLanguage @@ -134,7 +138,7 @@ class Sphinx: :ivar outdir: Directory for storing build documents. """ - warningiserror: bool + warningiserror: Final = False _warncount: int def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[str] | None, @@ -144,7 +148,7 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st freshenv: bool = False, warningiserror: bool = False, tags: Sequence[str] = (), verbosity: int = 0, parallel: int = 0, keep_going: bool = False, - pdb: bool = False) -> None: + pdb: bool = False, exception_on_warning: bool = False) -> None: """Initialize the Sphinx application. :param srcdir: The path to the source directory. @@ -163,8 +167,9 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st :param verbosity: The verbosity level. :param parallel: The maximum number of parallel jobs to use when reading/writing documents. - :param keep_going: If true, continue processing when an error occurs. + :param keep_going: Unused. :param pdb: If true, enable the Python debugger on an exception. + :param exception_on_warning: If true, raise an exception on warnings. """ self.phase = BuildPhase.INITIALIZATION self.verbosity = verbosity @@ -203,12 +208,10 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st else: self._warning = warning self._warncount = 0 - self.keep_going = warningiserror and keep_going - if self.keep_going: - self.warningiserror = False - else: - self.warningiserror = warningiserror + self.keep_going = bool(warningiserror) # Unused + self._fail_on_warnings = bool(warningiserror) self.pdb = pdb + self._exception_on_warning = exception_on_warning logging.setup(self, self._status, self._warning) self.events = EventManager(self) @@ -386,26 +389,31 @@ def build(self, force_all: bool = False, filenames: list[str] | None = None) -> self.events.emit('build-finished', err) raise - if self._warncount and self.keep_going: - self.statuscode = 1 - - status = (__('succeeded') if self.statuscode == 0 - else __('finished with problems')) - if self._warncount: - if self.warningiserror: - if self._warncount == 1: - msg = __('build %s, %s warning (with warnings treated as errors).') - else: - msg = __('build %s, %s warnings (with warnings treated as errors).') + if self._warncount == 0: + if self.statuscode != 0: + logger.info(bold(__('build finished with problems.'))) else: - if self._warncount == 1: - msg = __('build %s, %s warning.') - else: - msg = __('build %s, %s warnings.') - - logger.info(bold(msg), status, self._warncount) + logger.info(bold(__('build succeeded.'))) + elif self._warncount == 1: + if self._fail_on_warnings: + self.statuscode = 1 + msg = __('build finished with problems, 1 warning ' + '(with warnings treated as errors).') + elif self.statuscode != 0: + msg = __('build finished with problems, 1 warning.') + else: + msg = __('build succeeded, 1 warning.') + logger.info(bold(msg)) else: - logger.info(bold(__('build %s.')), status) + if self._fail_on_warnings: + self.statuscode = 1 + msg = __('build finished with problems, %s warnings ' + '(with warnings treated as errors).') + elif self.statuscode != 0: + msg = __('build finished with problems, %s warnings.') + else: + msg = __('build succeeded, %s warnings.') + logger.info(bold(msg), self._warncount) if self.statuscode == 0 and self.builder.epilog: logger.info('') @@ -450,6 +458,329 @@ def require_sphinx(version: tuple[int, int] | str) -> None: req = f'{major}.{minor}' raise VersionRequirementError(req) + # ---- Core events ------------------------------------------------------- + + @overload + def connect( + self, + event: Literal['config-inited'], + callback: Callable[[Sphinx, Config], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['builder-inited'], + callback: Callable[[Sphinx], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-get-outdated'], + callback: Callable[ + [Sphinx, BuildEnvironment, Set[str], Set[str], Set[str]], Sequence[str] + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-before-read-docs'], + callback: Callable[[Sphinx, BuildEnvironment, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-purge-doc'], + callback: Callable[[Sphinx, BuildEnvironment, str], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['source-read'], + callback: Callable[[Sphinx, str, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['include-read'], + callback: Callable[[Sphinx, Path, str, list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['doctree-read'], + callback: Callable[[Sphinx, nodes.document], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-merge-info'], + callback: Callable[ + [Sphinx, BuildEnvironment, list[str], BuildEnvironment], None + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-updated'], + callback: Callable[[Sphinx, BuildEnvironment], str], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-get-updated'], + callback: Callable[[Sphinx, BuildEnvironment], Iterable[str]], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['env-check-consistency'], + callback: Callable[[Sphinx, BuildEnvironment], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['write-started'], + callback: Callable[[Sphinx, Builder], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['doctree-resolved'], + callback: Callable[[Sphinx, nodes.document, str], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['missing-reference'], + callback: Callable[ + [Sphinx, BuildEnvironment, addnodes.pending_xref, nodes.TextElement], + nodes.reference | None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['warn-missing-reference'], + callback: Callable[[Sphinx, Domain, addnodes.pending_xref], bool | None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['build-finished'], + callback: Callable[[Sphinx, Exception | None], None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from builtin builders -------------------------------------- + + @overload + def connect( + self, + event: Literal['html-collect-pages'], + callback: Callable[[Sphinx], Iterable[tuple[str, dict[str, Any], str]]], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['html-page-context'], + callback: Callable[ + [Sphinx, str, str, dict[str, Any], nodes.document], str | None + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['linkcheck-process-uri'], + callback: Callable[[Sphinx, str], str | None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from builtin extensions-- ---------------------------------- + + @overload + def connect( + self, + event: Literal['object-description-transform'], + callback: Callable[[Sphinx, str, str, addnodes.desc_content], None], + priority: int = 500 + ) -> int: + ... + + # ---- Events from first-party extensions -------------------------------- + + @overload + def connect( + self, + event: Literal['autodoc-process-docstring'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + dict[str, bool], + Sequence[str], + ], + None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-before-process-signature'], + callback: Callable[[Sphinx, Any, bool], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-process-signature'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + dict[str, bool], + str | None, + str | None, + ], + tuple[str | None, str | None] | None, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-process-bases'], + callback: Callable[[Sphinx, str, Any, dict[str, bool], list[str]], None], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['autodoc-skip-member'], + callback: Callable[ + [ + Sphinx, + Literal['module', 'class', 'exception', 'function', 'method', 'attribute'], + str, + Any, + bool, + dict[str, bool], + ], + bool, + ], + priority: int = 500 + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['todo-defined'], + callback: Callable[[Sphinx, todo_node], None], + priority: int = 500, + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['viewcode-find-source'], + callback: Callable[ + [Sphinx, str], + tuple[str, dict[str, tuple[Literal['class', 'def', 'other'], int, int]]], + ], + priority: int = 500, + ) -> int: + ... + + @overload + def connect( + self, + event: Literal['viewcode-follow-imported'], + callback: Callable[[Sphinx, str, str], str | None], + priority: int = 500, + ) -> int: + ... + + # ---- Catch-all --------------------------------------------------------- + + @overload + def connect( + self, + event: str, + callback: Callable[..., Any], + priority: int = 500 + ) -> int: + ... + # event interface def connect(self, event: str, callback: Callable, priority: int = 500) -> int: """Register *callback* to be called when *event* is emitted. @@ -845,7 +1176,7 @@ def add_index_to_domain(self, domain: str, index: type[Index], _override: bool = def add_object_type(self, directivename: str, rolename: str, indextemplate: str = '', parse_node: Callable | None = None, - ref_nodeclass: type[TextElement] | None = None, + ref_nodeclass: type[nodes.TextElement] | None = None, objname: str = '', doc_field_types: Sequence = (), override: bool = False, ) -> None: @@ -912,9 +1243,11 @@ def add_object_type(self, directivename: str, rolename: str, indextemplate: str ref_nodeclass, objname, doc_field_types, override=override) - def add_crossref_type(self, directivename: str, rolename: str, indextemplate: str = '', - ref_nodeclass: type[TextElement] | None = None, objname: str = '', - override: bool = False) -> None: + def add_crossref_type( + self, directivename: str, rolename: str, indextemplate: str = '', + ref_nodeclass: type[nodes.TextElement] | None = None, objname: str = '', + override: bool = False, + ) -> None: """Register a new crossref object type. This method is very similar to :meth:`~Sphinx.add_object_type` except that the diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 3c5313afe41..76e7e230cdc 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -6,6 +6,7 @@ import pickle import re import time +from contextlib import nullcontext from os import path from typing import TYPE_CHECKING, Any, Literal, final @@ -327,7 +328,7 @@ def build( logger.info(bold(__('building [%s]: ')) + summary, self.name) # while reading, collect all warnings from docutils - with logging.pending_warnings(): + with nullcontext() if self.app._exception_on_warning else logging.pending_warnings(): updated_docnames = set(self.read()) doccount = len(updated_docnames) @@ -627,7 +628,7 @@ def write( self._write_serial(sorted(docnames)) def _write_serial(self, docnames: Sequence[str]) -> None: - with logging.pending_warnings(): + with nullcontext() if self.app._exception_on_warning else logging.pending_warnings(): for docname in status_iterator(docnames, __('writing output... '), "darkgreen", len(docnames), self.app.verbosity): self.app.phase = BuildPhase.RESOLVING diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 5352b25936b..e9b07164eaa 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -106,7 +106,7 @@ def process_result(self, result: CheckResult) -> None: elif result.status == 'working': logger.info(darkgreen('ok ') + result.uri + result.message) elif result.status == 'timeout': - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: logger.warning('timeout ' + result.uri + result.message, location=(result.docname, result.lineno)) else: @@ -115,7 +115,7 @@ def process_result(self, result: CheckResult) -> None: result.uri + ': ' + result.message) self.timed_out_hyperlinks += 1 elif result.status == 'broken': - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: logger.warning(__('broken link: %s (%s)'), result.uri, result.message, location=(result.docname, result.lineno)) else: diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index 02fd99a8ccc..9f6cf2a3363 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -201,12 +201,17 @@ def get_parser() -> argparse.ArgumentParser: help=__('write warnings (and errors) to given file')) group.add_argument('--fail-on-warning', '-W', action='store_true', dest='warningiserror', help=__('turn warnings into errors')) - group.add_argument('--keep-going', action='store_true', dest='keep_going', - help=__("with --fail-on-warning, keep going when getting warnings")) + group.add_argument('--keep-going', action='store_true', help=argparse.SUPPRESS) group.add_argument('--show-traceback', '-T', action='store_true', dest='traceback', help=__('show full traceback on exception')) group.add_argument('--pdb', '-P', action='store_true', dest='pdb', help=__('run Pdb on exception')) + group.add_argument('--exception-on-warning', action='store_true', + dest='exception_on_warning', + help=__('raise an exception on warnings')) + + if parser.prog == '__main__.py': + parser.prog = 'sphinx-build' return parser @@ -329,11 +334,16 @@ def build_main(argv: Sequence[str]) -> int: try: confdir = args.confdir or args.sourcedir with patch_docutils(confdir), docutils_namespace(): - app = Sphinx(args.sourcedir, args.confdir, args.outputdir, - args.doctreedir, args.builder, args.confoverrides, args.status, - args.warning, args.freshenv, args.warningiserror, - args.tags, args.verbosity, args.jobs, args.keep_going, - args.pdb) + app = Sphinx( + srcdir=args.sourcedir, confdir=args.confdir, + outdir=args.outputdir, doctreedir=args.doctreedir, + buildername=args.builder, confoverrides=args.confoverrides, + status=args.status, warning=args.warning, + freshenv=args.freshenv, warningiserror=args.warningiserror, + tags=args.tags, + verbosity=args.verbosity, parallel=args.jobs, keep_going=False, + pdb=args.pdb, exception_on_warning=args.exception_on_warning, + ) app.build(args.force_all, args.filenames) return app.statuscode except (Exception, KeyboardInterrupt) as exc: @@ -379,7 +389,8 @@ def main(argv: Sequence[str] = (), /) -> int: if argv[:1] == ['--bug-report']: return _bug_report_info() if argv[:1] == ['-M']: - return make_main(argv) + from sphinx.cmd import make_mode + return make_mode.run_make_mode(argv[1:]) else: return build_main(argv) diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index 65df9f6227e..d1ba3fccf9c 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -58,31 +58,31 @@ class Make: - def __init__(self, srcdir: str, builddir: str, opts: Sequence[str]) -> None: - self.srcdir = srcdir - self.builddir = builddir + def __init__(self, *, source_dir: str, build_dir: str, opts: Sequence[str]) -> None: + self.source_dir = source_dir + self.build_dir = build_dir self.opts = [*opts] - def builddir_join(self, *comps: str) -> str: - return path.join(self.builddir, *comps) + def build_dir_join(self, *comps: str) -> str: + return path.join(self.build_dir, *comps) def build_clean(self) -> int: - srcdir = path.abspath(self.srcdir) - builddir = path.abspath(self.builddir) - if not path.exists(self.builddir): + source_dir = path.abspath(self.source_dir) + build_dir = path.abspath(self.build_dir) + if not path.exists(self.build_dir): return 0 - elif not path.isdir(self.builddir): - print("Error: %r is not a directory!" % self.builddir) + elif not path.isdir(self.build_dir): + print("Error: %r is not a directory!" % self.build_dir) return 1 - elif srcdir == builddir: - print("Error: %r is same as source directory!" % self.builddir) + elif source_dir == build_dir: + print("Error: %r is same as source directory!" % self.build_dir) return 1 - elif path.commonpath([srcdir, builddir]) == builddir: - print("Error: %r directory contains source directory!" % self.builddir) + elif path.commonpath([source_dir, build_dir]) == build_dir: + print("Error: %r directory contains source directory!" % self.build_dir) return 1 - print("Removing everything under %r..." % self.builddir) - for item in os.listdir(self.builddir): - rmtree(self.builddir_join(item)) + print("Removing everything under %r..." % self.build_dir) + for item in os.listdir(self.build_dir): + rmtree(self.build_dir_join(item)) return 0 def build_help(self) -> None: @@ -105,7 +105,7 @@ def build_latexpdf(self) -> int: if not makecmd.lower().startswith('make'): raise RuntimeError('Invalid $MAKE command: %r' % makecmd) try: - with chdir(self.builddir_join('latex')): + with chdir(self.build_dir_join('latex')): if '-Q' in self.opts: with open('__LATEXSTDOUT__', 'w') as outfile: returncode = subprocess.call([makecmd, @@ -117,7 +117,7 @@ def build_latexpdf(self) -> int: ) if returncode: print('Latex error: check %s' % - self.builddir_join('latex', '__LATEXSTDOUT__') + self.build_dir_join('latex', '__LATEXSTDOUT__') ) elif '-q' in self.opts: returncode = subprocess.call( @@ -129,7 +129,7 @@ def build_latexpdf(self) -> int: ) if returncode: print('Latex error: check .log file in %s' % - self.builddir_join('latex') + self.build_dir_join('latex') ) else: returncode = subprocess.call([makecmd, 'all-pdf']) @@ -148,7 +148,7 @@ def build_latexpdfja(self) -> int: if not makecmd.lower().startswith('make'): raise RuntimeError('Invalid $MAKE command: %r' % makecmd) try: - with chdir(self.builddir_join('latex')): + with chdir(self.build_dir_join('latex')): return subprocess.call([makecmd, 'all-pdf']) except OSError: print('Error: Failed to run: %s' % makecmd) @@ -163,32 +163,33 @@ def build_info(self) -> int: if not makecmd.lower().startswith('make'): raise RuntimeError('Invalid $MAKE command: %r' % makecmd) try: - with chdir(self.builddir_join('texinfo')): + with chdir(self.build_dir_join('texinfo')): return subprocess.call([makecmd, 'info']) except OSError: print('Error: Failed to run: %s' % makecmd) return 1 def build_gettext(self) -> int: - dtdir = self.builddir_join('gettext', '.doctrees') + dtdir = self.build_dir_join('gettext', '.doctrees') if self.run_generic_build('gettext', doctreedir=dtdir) > 0: return 1 return 0 def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: # compatibility with old Makefile - papersize = os.getenv('PAPER', '') - opts = self.opts - if papersize in ('a4', 'letter'): - opts.extend(['-D', 'latex_elements.papersize=' + papersize + 'paper']) + paper_size = os.getenv('PAPER', '') + if paper_size in {'a4', 'letter'}: + self.opts.extend(['-D', f'latex_elements.papersize={paper_size}paper']) if doctreedir is None: - doctreedir = self.builddir_join('doctrees') + doctreedir = self.build_dir_join('doctrees') - args = ['-b', builder, - '-d', doctreedir, - self.srcdir, - self.builddir_join(builder)] - return build_main(args + opts) + args = [ + '--builder', builder, + '--doctree-dir', doctreedir, + self.source_dir, + self.build_dir_join(builder), + ] + return build_main(args + self.opts) def run_make_mode(args: Sequence[str]) -> int: @@ -196,8 +197,10 @@ def run_make_mode(args: Sequence[str]) -> int: print('Error: at least 3 arguments (builder, source ' 'dir, build dir) are required.', file=sys.stderr) return 1 - make = Make(args[1], args[2], args[3:]) - run_method = 'build_' + args[0] + + builder_name = args[0] + make = Make(source_dir=args[1], build_dir=args[2], opts=args[3:]) + run_method = f'build_{builder_name}' if hasattr(make, run_method): return getattr(make, run_method)() - return make.run_generic_build(args[0]) + return make.run_generic_build(builder_name) diff --git a/sphinx/config.py b/sphinx/config.py index bd5941e2cae..bae92140d59 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: import os - from collections.abc import Collection, Iterator, Sequence, Set + from collections.abc import Collection, Iterable, Iterator, Sequence, Set from typing import TypeAlias from sphinx.application import Sphinx @@ -739,8 +739,8 @@ def check_primary_domain(app: Sphinx, config: Config) -> None: config.primary_domain = None -def check_root_doc(app: Sphinx, env: BuildEnvironment, added: set[str], - changed: set[str], removed: set[str]) -> set[str]: +def check_root_doc(app: Sphinx, env: BuildEnvironment, added: Set[str], + changed: Set[str], removed: Set[str]) -> Iterable[str]: """Adjust root_doc to 'contents' to support an old project which does not have any root_doc setting. """ diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index b05643e1436..ef3b5e90ade 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -7,20 +7,16 @@ from __future__ import annotations import copy -from abc import ABC, abstractmethod -from collections.abc import Callable -from typing import TYPE_CHECKING, Any, NamedTuple, cast +from typing import TYPE_CHECKING, Any, cast -from docutils.nodes import Element, Node, system_message - -from sphinx.errors import SphinxError +from sphinx.domains._index import Index, IndexEntry from sphinx.locale import _ if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - from typing import TypeAlias + from collections.abc import Callable, Iterable, Sequence from docutils import nodes + from docutils.nodes import Element, Node from docutils.parsers.rst import Directive from docutils.parsers.rst.states import Inliner @@ -28,7 +24,14 @@ from sphinx.builders import Builder from sphinx.environment import BuildEnvironment from sphinx.roles import XRefRole - from sphinx.util.typing import RoleFunction + from sphinx.util.typing import RoleFunction, TitleGetter + +__all__ = ( + 'Domain', + 'Index', + 'IndexEntry', + 'ObjType', +) class ObjType: @@ -50,112 +53,10 @@ class ObjType: 'searchprio': 1, } - def __init__(self, lname: str, *roles: Any, **attrs: Any) -> None: - self.lname = lname - self.roles: tuple = roles - self.attrs: dict = self.known_attrs.copy() - self.attrs.update(attrs) - - -class IndexEntry(NamedTuple): - name: str - subtype: int - docname: str - anchor: str - extra: str - qualifier: str - descr: str - - -class Index(ABC): - """ - An Index is the description for a domain-specific index. To add an index to - a domain, subclass Index, overriding the three name attributes: - - * `name` is an identifier used for generating file names. - It is also used for a hyperlink target for the index. Therefore, users can - refer the index page using ``ref`` role and a string which is combined - domain name and ``name`` attribute (ex. ``:ref:`py-modindex```). - * `localname` is the section title for the index. - * `shortname` is a short name for the index, for use in the relation bar in - HTML output. Can be empty to disable entries in the relation bar. - - and providing a :meth:`generate()` method. Then, add the index class to - your domain's `indices` list. Extensions can add indices to existing - domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. - - .. versionchanged:: 3.0 - - Index pages can be referred by domain name and index name via - :rst:role:`ref` role. - """ - - name: str - localname: str - shortname: str | None = None - - def __init__(self, domain: Domain) -> None: - if not self.name or self.localname is None: - raise SphinxError('Index subclass %s has no valid name or localname' - % self.__class__.__name__) - self.domain = domain - - @abstractmethod - def generate(self, docnames: Iterable[str] | None = None, - ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: - """Get entries for the index. - - If ``docnames`` is given, restrict to entries referring to these - docnames. - - The return value is a tuple of ``(content, collapse)``: - - ``collapse`` - A boolean that determines if sub-entries should start collapsed (for - output formats that support collapsing sub-entries). - - ``content``: - A sequence of ``(letter, entries)`` tuples, where ``letter`` is the - "heading" for the given ``entries``, usually the starting letter, and - ``entries`` is a sequence of single entries. Each entry is a sequence - ``[name, subtype, docname, anchor, extra, qualifier, descr]``. The - items in this sequence have the following meaning: - - ``name`` - The name of the index entry to be displayed. - - ``subtype`` - The sub-entry related type. One of: - - ``0`` - A normal entry. - ``1`` - An entry with sub-entries. - ``2`` - A sub-entry. - - ``docname`` - *docname* where the entry is located. - - ``anchor`` - Anchor for the entry within ``docname`` - - ``extra`` - Extra info for the entry. - - ``qualifier`` - Qualifier for the description. - - ``descr`` - Description for the entry. - - Qualifier and description are not rendered for some output formats such - as LaTeX. - """ - raise NotImplementedError - - -TitleGetter: TypeAlias = Callable[[Node], str | None] + def __init__(self, lname: str, /, *roles: Any, **attrs: Any) -> None: + self.lname: str = lname + self.roles: tuple[Any, ...] = roles + self.attrs: dict[str, Any] = self.known_attrs | attrs class Domain: @@ -268,7 +169,7 @@ def role(self, name: str) -> RoleFunction | None: def role_adapter(typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict | None = None, content: Sequence[str] = (), - ) -> tuple[list[Node], list[system_message]]: + ) -> tuple[list[Node], list[nodes.system_message]]: return self.roles[name](fullname, rawtext, text, lineno, inliner, options or {}, content) self._role_cache[name] = role_adapter diff --git a/sphinx/domains/_index.py b/sphinx/domains/_index.py new file mode 100644 index 00000000000..1598d2f2026 --- /dev/null +++ b/sphinx/domains/_index.py @@ -0,0 +1,113 @@ +"""Domain indices.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, NamedTuple + +from sphinx.errors import SphinxError + +if TYPE_CHECKING: + from collections.abc import Iterable + + from sphinx.domains import Domain + + +class IndexEntry(NamedTuple): + name: str + subtype: int + docname: str + anchor: str + extra: str + qualifier: str + descr: str + + +class Index(ABC): + """ + An Index is the description for a domain-specific index. To add an index to + a domain, subclass Index, overriding the three name attributes: + + * `name` is an identifier used for generating file names. + It is also used for a hyperlink target for the index. Therefore, users can + refer the index page using ``ref`` role and a string which is combined + domain name and ``name`` attribute (ex. ``:ref:`py-modindex```). + * `localname` is the section title for the index. + * `shortname` is a short name for the index, for use in the relation bar in + HTML output. Can be empty to disable entries in the relation bar. + + and providing a :meth:`generate()` method. Then, add the index class to + your domain's `indices` list. Extensions can add indices to existing + domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`. + + .. versionchanged:: 3.0 + + Index pages can be referred by domain name and index name via + :rst:role:`ref` role. + """ + + name: str + localname: str + shortname: str | None = None + + def __init__(self, domain: Domain) -> None: + if not self.name or self.localname is None: + msg = f'Index subclass {self.__class__.__name__} has no valid name or localname' + raise SphinxError(msg) + self.domain = domain + + @abstractmethod + def generate( + self, + docnames: Iterable[str] | None = None, + ) -> tuple[list[tuple[str, list[IndexEntry]]], bool]: + """Get entries for the index. + + If ``docnames`` is given, restrict to entries referring to these + docnames. + + The return value is a tuple of ``(content, collapse)``: + + ``collapse`` + A boolean that determines if sub-entries should start collapsed (for + output formats that support collapsing sub-entries). + + ``content``: + A sequence of ``(letter, entries)`` tuples, where ``letter`` is the + "heading" for the given ``entries``, usually the starting letter, and + ``entries`` is a sequence of single entries. Each entry is a sequence + ``[name, subtype, docname, anchor, extra, qualifier, descr]``. The + items in this sequence have the following meaning: + + ``name`` + The name of the index entry to be displayed. + + ``subtype`` + The sub-entry related type. One of: + + ``0`` + A normal entry. + ``1`` + An entry with sub-entries. + ``2`` + A sub-entry. + + ``docname`` + *docname* where the entry is located. + + ``anchor`` + Anchor for the entry within ``docname`` + + ``extra`` + Extra info for the entry. + + ``qualifier`` + Qualifier for the description. + + ``descr`` + Description for the entry. + + Qualifier and description are not rendered for some output formats such + as LaTeX. + """ + raise NotImplementedError diff --git a/sphinx/domains/std/__init__.py b/sphinx/domains/std/__init__.py index 629cad3a5cd..882a8a7bf1b 100644 --- a/sphinx/domains/std/__init__.py +++ b/sphinx/domains/std/__init__.py @@ -14,7 +14,7 @@ from sphinx import addnodes from sphinx.addnodes import desc_signature, pending_xref from sphinx.directives import ObjectDescription -from sphinx.domains import Domain, ObjType, TitleGetter +from sphinx.domains import Domain, ObjType from sphinx.locale import _, __ from sphinx.roles import EmphasizedLiteral, XRefRole from sphinx.util import docname_join, logging, ws_re @@ -28,7 +28,12 @@ from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.environment import BuildEnvironment - from sphinx.util.typing import ExtensionMetadata, OptionSpec, RoleFunction + from sphinx.util.typing import ( + ExtensionMetadata, + OptionSpec, + RoleFunction, + TitleGetter, + ) logger = logging.getLogger(__name__) diff --git a/sphinx/events.py b/sphinx/events.py index df2fdf7048d..17de456f033 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -7,7 +7,7 @@ from collections import defaultdict from operator import attrgetter -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING, NamedTuple, overload from sphinx.errors import ExtensionError, SphinxError from sphinx.locale import __ @@ -15,9 +15,19 @@ from sphinx.util.inspect import safe_getattr if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Iterable, Sequence, Set + from pathlib import Path + from typing import Any, Literal + from docutils import nodes + + from sphinx import addnodes from sphinx.application import Sphinx + from sphinx.builders import Builder + from sphinx.config import Config + from sphinx.domains import Domain + from sphinx.environment import BuildEnvironment + from sphinx.ext.todo import todo_node logger = logging.getLogger(__name__) @@ -66,6 +76,305 @@ def add(self, name: str) -> None: raise ExtensionError(__('Event %r already present') % name) self.events[name] = '' + # ---- Core events ------------------------------------------------------- + + @overload + def connect( + self, + name: Literal['config-inited'], + callback: Callable[[Sphinx, Config], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['builder-inited'], + callback: Callable[[Sphinx], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-get-outdated'], + callback: Callable[ + [Sphinx, BuildEnvironment, Set[str], Set[str], Set[str]], Sequence[str] + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-before-read-docs'], + callback: Callable[[Sphinx, BuildEnvironment, list[str]], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-purge-doc'], + callback: Callable[[Sphinx, BuildEnvironment, str], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['source-read'], + callback: Callable[[Sphinx, str, list[str]], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['include-read'], + callback: Callable[[Sphinx, Path, str, list[str]], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['doctree-read'], + callback: Callable[[Sphinx, nodes.document], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-merge-info'], + callback: Callable[ + [Sphinx, BuildEnvironment, list[str], BuildEnvironment], None + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-updated'], + callback: Callable[[Sphinx, BuildEnvironment], str], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-get-updated'], + callback: Callable[[Sphinx, BuildEnvironment], Iterable[str]], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['env-check-consistency'], + callback: Callable[[Sphinx, BuildEnvironment], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['write-started'], + callback: Callable[[Sphinx, Builder], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['doctree-resolved'], + callback: Callable[[Sphinx, nodes.document, str], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['missing-reference'], + callback: Callable[ + [Sphinx, BuildEnvironment, addnodes.pending_xref, nodes.TextElement], + nodes.reference | None, + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['warn-missing-reference'], + callback: Callable[[Sphinx, Domain, addnodes.pending_xref], bool | None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['build-finished'], + callback: Callable[[Sphinx, Exception | None], None], + priority: int, + ) -> int: ... + + # ---- Events from builtin builders -------------------------------------- + + @overload + def connect( + self, + name: Literal['html-collect-pages'], + callback: Callable[[Sphinx], Iterable[tuple[str, dict[str, Any], str]]], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['html-page-context'], + callback: Callable[ + [Sphinx, str, str, dict[str, Any], nodes.document], str | None + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['linkcheck-process-uri'], + callback: Callable[[Sphinx, str], str | None], + priority: int, + ) -> int: ... + + # ---- Events from builtin extensions-- ---------------------------------- + + @overload + def connect( + self, + name: Literal['object-description-transform'], + callback: Callable[[Sphinx, str, str, addnodes.desc_content], None], + priority: int, + ) -> int: ... + + # ---- Events from first-party extensions -------------------------------- + + @overload + def connect( + self, + name: Literal['autodoc-process-docstring'], + callback: Callable[ + [ + Sphinx, + Literal[ + 'module', 'class', 'exception', 'function', 'method', 'attribute' + ], + str, + Any, + dict[str, bool], + Sequence[str], + ], + None, + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['autodoc-before-process-signature'], + callback: Callable[[Sphinx, Any, bool], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['autodoc-process-signature'], + callback: Callable[ + [ + Sphinx, + Literal[ + 'module', 'class', 'exception', 'function', 'method', 'attribute' + ], + str, + Any, + dict[str, bool], + str | None, + str | None, + ], + tuple[str | None, str | None] | None, + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['autodoc-process-bases'], + callback: Callable[[Sphinx, str, Any, dict[str, bool], list[str]], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['autodoc-skip-member'], + callback: Callable[ + [ + Sphinx, + Literal[ + 'module', 'class', 'exception', 'function', 'method', 'attribute' + ], + str, + Any, + bool, + dict[str, bool], + ], + bool, + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['todo-defined'], + callback: Callable[[Sphinx, todo_node], None], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['viewcode-find-source'], + callback: Callable[ + [Sphinx, str], + tuple[str, dict[str, tuple[Literal['class', 'def', 'other'], int, int]]], + ], + priority: int, + ) -> int: ... + + @overload + def connect( + self, + name: Literal['viewcode-follow-imported'], + callback: Callable[[Sphinx, str, str], str | None], + priority: int, + ) -> int: ... + + # ---- Catch-all --------------------------------------------------------- + + @overload + def connect( + self, + name: str, + callback: Callable[..., Any], + priority: int, + ) -> int: ... + def connect(self, name: str, callback: Callable, priority: int) -> int: """Connect a handler to specific event.""" if name not in self.events: diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index e318d83a00e..28559e0eccc 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -415,9 +415,10 @@ def import_object(self, raiseerror: bool = False) -> bool: """ with mock(self.config.autodoc_mock_imports): try: - ret = import_object(self.modname, self.objpath, self.objtype, - attrgetter=self.get_attr, - warningiserror=self.config.autodoc_warningiserror) + ret = import_object( + self.modname, self.objpath, self.objtype, + attrgetter=self.get_attr, + ) self.module, self.parent, self.object_name, self.object = ret if ismock(self.object): self.object = undecorate(self.object) @@ -1960,7 +1961,7 @@ def import_object(self, raiseerror: bool = False) -> bool: # annotation only instance variable (PEP-526) try: with mock(self.config.autodoc_mock_imports): - parent = import_module(self.modname, self.config.autodoc_warningiserror) + parent = import_module(self.modname) annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases, include_extras=True) @@ -2455,9 +2456,10 @@ def import_object(self, raiseerror: bool = False) -> bool: except ImportError as exc: try: with mock(self.config.autodoc_mock_imports): - ret = import_object(self.modname, self.objpath[:-1], 'class', - attrgetter=self.get_attr, # type: ignore[attr-defined] - warningiserror=self.config.autodoc_warningiserror) + ret = import_object( + self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore[attr-defined] + ) parent = ret[3] if self.is_runtime_instance_attribute(parent): self.object = self.RUNTIME_INSTANCE_ATTRIBUTE @@ -2509,9 +2511,10 @@ def import_object(self, raiseerror: bool = False) -> bool: return super().import_object(raiseerror=True) # type: ignore[misc] except ImportError as exc: try: - ret = import_object(self.modname, self.objpath[:-1], 'class', - attrgetter=self.get_attr, # type: ignore[attr-defined] - warningiserror=self.config.autodoc_warningiserror) + ret = import_object( + self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, # type: ignore[attr-defined] + ) parent = ret[3] if self.is_uninitialized_instance_attribute(parent): self.object = UNINITIALIZED_ATTR diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index dd28146b0dd..ebdaa984888 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -137,24 +137,22 @@ def unmangle(subject: Any, name: str) -> str | None: return name -def import_module(modname: str, warningiserror: bool = False) -> Any: +def import_module(modname: str) -> Any: """Call importlib.import_module(modname), convert exceptions to ImportError.""" try: - with logging.skip_warningiserror(not warningiserror): - return importlib.import_module(modname) + return importlib.import_module(modname) except BaseException as exc: # Importing modules may cause any side effects, including # SystemExit, so we need to catch all errors. raise ImportError(exc, traceback.format_exc()) from exc -def _reload_module(module: ModuleType, warningiserror: bool = False) -> Any: +def _reload_module(module: ModuleType) -> Any: """ Call importlib.reload(module), convert exceptions to ImportError """ try: - with logging.skip_warningiserror(not warningiserror): - return importlib.reload(module) + return importlib.reload(module) except BaseException as exc: # Importing modules may cause any side effects, including # SystemExit, so we need to catch all errors. @@ -162,8 +160,7 @@ def _reload_module(module: ModuleType, warningiserror: bool = False) -> Any: def import_object(modname: str, objpath: list[str], objtype: str = '', - attrgetter: Callable[[Any, str], Any] = safe_getattr, - warningiserror: bool = False) -> Any: + attrgetter: Callable[[Any, str], Any] = safe_getattr) -> Any: if objpath: logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath)) else: @@ -176,7 +173,7 @@ def import_object(modname: str, objpath: list[str], objtype: str = '', while module is None: try: original_module_names = frozenset(sys.modules) - module = import_module(modname, warningiserror=warningiserror) + module = import_module(modname) if os.environ.get('SPHINX_AUTODOC_RELOAD_MODULES'): new_modules = [m for m in sys.modules if m not in original_module_names] # Try reloading modules with ``typing.TYPE_CHECKING == True``. diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index bed3e5b8a37..b6f31be9b3b 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -71,7 +71,7 @@ def __init__(self, translator: NullTranslations) -> None: self.translator = translator self.verbosity = 0 self._warncount = 0 - self.warningiserror = False + self._exception_on_warning = False self.config.add('autosummary_context', {}, 'env', ()) self.config.add('autosummary_filename_map', {}, 'env', ()) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 5ecc1670959..c075e954883 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -241,7 +241,7 @@ def write_c_coverage(self) -> None: for typ, name in sorted(undoc): op.write(' * %-50s [%9s]\n' % (name, typ)) if self.config.coverage_show_missing_items: - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: logger.warning(__('undocumented c api: %s [%s] in file %s'), name, typ, filename) else: @@ -423,7 +423,7 @@ def write_py_coverage(self) -> None: op.write('Functions:\n') op.writelines(' * %s\n' % x for x in undoc['funcs']) if self.config.coverage_show_missing_items: - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: for func in undoc['funcs']: logger.warning( __('undocumented python function: %s :: %s'), @@ -440,7 +440,7 @@ def write_py_coverage(self) -> None: if not methods: op.write(' * %s\n' % class_name) if self.config.coverage_show_missing_items: - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: logger.warning( __('undocumented python class: %s :: %s'), name, class_name) @@ -452,7 +452,7 @@ def write_py_coverage(self) -> None: op.write(' * %s -- missing methods:\n\n' % class_name) op.writelines(' - %s\n' % x for x in methods) if self.config.coverage_show_missing_items: - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: for meth in methods: logger.warning( __('undocumented python method:' diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 19f01f4cab6..ba451208a5e 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -322,7 +322,7 @@ def _out(self, text: str) -> None: self.outfile.write(text) def _warn_out(self, text: str) -> None: - if self.app.quiet or self.app.warningiserror: + if self.app.quiet: logger.warning(text) else: logger.info(text, nonl=True) diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index 7e7811e8907..03e38e85e86 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -28,7 +28,7 @@ 'testroot="root", srcdir=None, ' 'confoverrides=None, freshenv=False, ' 'warningiserror=False, tags=None, verbosity=0, parallel=0, ' - 'keep_going=False, builddir=None, docutils_conf=None' + 'builddir=None, docutils_conf=None' '): arguments to initialize the sphinx test application.' ), 'test_params(shared_result=...): test parameters.', @@ -239,7 +239,7 @@ def if_graphviz_found(app: SphinxTestApp) -> None: # NoQA: PT004 @pytest.fixture(scope='session') -def sphinx_test_tempdir(tmp_path_factory: Any) -> Path: +def sphinx_test_tempdir(tmp_path_factory: pytest.TempPathFactory) -> Path: """Temporary directory.""" return tmp_path_factory.getbasetemp() diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 3a9ee8ddb20..49f0ffa6005 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -178,7 +178,7 @@ def read_bytes(self) -> builtins.bytes: with open(self, mode='rb') as f: return f.read() - def write_bytes(self, bytes: str, append: bool = False) -> None: + def write_bytes(self, bytes: bytes, append: bool = False) -> None: """ Writes the given `bytes` to the file. diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 53e55d6d5ce..9cf77d30439 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -86,7 +86,7 @@ class SphinxTestApp(sphinx.application.Sphinx): It is recommended to use:: - @pytest.mark.sphinx('html') + @pytest.mark.sphinx('html', testroot='root') def test(app): app = ... @@ -117,8 +117,9 @@ def __init__( parallel: int = 0, # additional arguments at the end to keep the signature verbosity: int = 0, # argument is not in the same order as in the superclass - keep_going: bool = False, warningiserror: bool = False, # argument is not in the same order as in the superclass + pdb: bool = False, + exception_on_warning: bool = False, # unknown keyword arguments **extras: Any, ) -> None: @@ -170,13 +171,23 @@ def __init__( srcdir, confdir, outdir, doctreedir, buildername, confoverrides=confoverrides, status=status, warning=warning, freshenv=freshenv, warningiserror=warningiserror, tags=tags, - verbosity=verbosity, parallel=parallel, keep_going=keep_going, - pdb=False, + verbosity=verbosity, parallel=parallel, + pdb=pdb, exception_on_warning=exception_on_warning, ) except Exception: self.cleanup() raise + def _init_builder(self) -> None: + # override the default theme to 'basic' rather than 'alabaster' + # for test independence + + if 'html_theme' in self.config._overrides: + pass # respect overrides + elif 'html_theme' in self.config and self.config.html_theme == 'alabaster': + self.config.html_theme = self.config._overrides.get('html_theme', 'basic') + super()._init_builder() + @property def status(self) -> StringIO: """The in-memory text I/O for the application status messages.""" diff --git a/sphinx/texinputs/sphinxlatexgraphics.sty b/sphinx/texinputs/sphinxlatexgraphics.sty index f30101382c4..f0c7c25f0ca 100644 --- a/sphinx/texinputs/sphinxlatexgraphics.sty +++ b/sphinx/texinputs/sphinxlatexgraphics.sty @@ -1,7 +1,7 @@ %% GRAPHICS % % change this info string if making any custom modification -\ProvidesPackage{sphinxlatexgraphics}[2021/01/27 graphics] +\ProvidesPackage{sphinxlatexgraphics}[2024/08/13 v8.1.0 graphics] % Provides support for this output mark-up from Sphinx latex writer: % @@ -84,7 +84,8 @@ \ifin@ \setbox\spx@image@box \hbox{\includegraphics - [%#1,% contained only width and/or height and overruled anyhow + [#1,% contains only width and/or height which are overruled next + % but in future may contain page=N hence must be kept width=\spx@image@requiredwidth,height=\spx@image@requiredheight]% {#2}}% % \includegraphics does not set box dimensions to the exactly diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 4816bcbbe00..804ef62ccb4 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -5,7 +5,7 @@ import logging import logging.handlers from collections import defaultdict -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from typing import IO, TYPE_CHECKING, Any from docutils import nodes @@ -17,6 +17,7 @@ if TYPE_CHECKING: from collections.abc import Iterator, Sequence, Set + from typing import NoReturn from docutils.nodes import Node @@ -322,24 +323,7 @@ def pending_logging() -> Iterator[MemoryHandler]: memhandler.flushTo(logger) -@contextmanager -def skip_warningiserror(skip: bool = True) -> Iterator[None]: - """Context manager to skip WarningIsErrorFilter temporarily.""" - logger = logging.getLogger(NAMESPACE) - - if skip is False: - yield - else: - try: - disabler = DisableWarningIsErrorFilter() - for handler in logger.handlers: - # use internal method; filters.insert() directly to install disabler - # before WarningIsErrorFilter - handler.filters.insert(0, disabler) - yield - finally: - for handler in logger.handlers: - handler.removeFilter(disabler) +skip_warningiserror = nullcontext # Deprecate in Sphinx 10 @contextmanager @@ -407,6 +391,21 @@ def filter(self, record: logging.LogRecord) -> bool: return record.levelno < logging.WARNING +class _RaiseOnWarningFilter(logging.Filter): + """Raise exception if a warning is emitted.""" + + def filter(self, record: logging.LogRecord) -> NoReturn: + try: + message = record.msg % record.args + except (TypeError, ValueError): + message = record.msg # use record.msg itself + if location := getattr(record, 'location', ''): + message = f"{location}:{message}" + if record.exc_info is not None: + raise SphinxWarning(message) from record.exc_info[1] + raise SphinxWarning(message) + + def is_suppressed_warning( warning_type: str, sub_type: str, suppress_warnings: Set[str] | Sequence[str], ) -> bool: @@ -445,44 +444,6 @@ def filter(self, record: logging.LogRecord) -> bool: return True -class WarningIsErrorFilter(logging.Filter): - """Raise exception if warning emitted.""" - - def __init__(self, app: Sphinx) -> None: - self.app = app - super().__init__() - - def filter(self, record: logging.LogRecord) -> bool: - if getattr(record, 'skip_warningsiserror', False): - # disabled by DisableWarningIsErrorFilter - return True - elif self.app.warningiserror: - location = getattr(record, 'location', '') - try: - message = record.msg % record.args - except (TypeError, ValueError): - message = record.msg # use record.msg itself - - if location: - exc = SphinxWarning(location + ":" + str(message)) - else: - exc = SphinxWarning(message) - if record.exc_info is not None: - raise exc from record.exc_info[1] - else: - raise exc - else: - return True - - -class DisableWarningIsErrorFilter(logging.Filter): - """Disable WarningIsErrorFilter if this filter installed.""" - - def filter(self, record: logging.LogRecord) -> bool: - record.skip_warningsiserror = True - return True - - class MessagePrefixFilter(logging.Filter): """Prepend prefix to all log records.""" @@ -653,9 +614,10 @@ def setup(app: Sphinx, status: IO, warning: IO) -> None: info_handler.setFormatter(ColorizeFormatter()) warning_handler = WarningStreamHandler(SafeEncodingWriter(warning)) + if app._exception_on_warning: + warning_handler.addFilter(_RaiseOnWarningFilter()) warning_handler.addFilter(WarningSuppressor(app)) warning_handler.addFilter(WarningLogRecordTranslator(app)) - warning_handler.addFilter(WarningIsErrorFilter(app)) warning_handler.addFilter(OnceFilter()) warning_handler.setLevel(logging.WARNING) warning_handler.setFormatter(ColorizeFormatter()) diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index 32912e014d7..f17ef71294b 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) # our parallel functionality only works for the forking Process -parallel_available = multiprocessing and os.name == 'posix' +parallel_available = HAS_MULTIPROCESSING and os.name == 'posix' class SerialTasks: diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 08acccec806..dbad5457ce5 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -23,6 +23,8 @@ from docutils import nodes from docutils.parsers.rst.states import Inliner +from sphinx.util import logging + if TYPE_CHECKING: from collections.abc import Mapping from typing import Final, Literal, Protocol, TypeAlias @@ -41,6 +43,8 @@ 'smart', ] +logger = logging.getLogger(__name__) + # classes that have an incorrect .__module__ attribute _INVALID_BUILTIN_CLASSES: Final[Mapping[object, str]] = { @@ -336,7 +340,8 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s else: # not a class (ex. TypeVar) but should have a __name__ return f':py:obj:`{module_prefix}{cls.__module__}.{cls.__name__}`' - except (AttributeError, TypeError): + except (AttributeError, TypeError) as exc: + logger.debug('restify on %r in mode %r failed: %r', cls, mode, exc) return object_description(cls) diff --git a/tests/js/jasmine-browser.mjs b/tests/js/jasmine-browser.mjs index 7cc8610dd76..b84217fd8c5 100644 --- a/tests/js/jasmine-browser.mjs +++ b/tests/js/jasmine-browser.mjs @@ -23,6 +23,6 @@ export default { hostname: "127.0.0.1", browser: { - name: "firefox" + name: "headlessFirefox" } }; diff --git a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py index c0928605a9e..234732ddc0c 100644 --- a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py +++ b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py @@ -2,34 +2,33 @@ Does foo.svg --> foo.pdf with no change to the file. """ +from __future__ import annotations + import shutil +from typing import TYPE_CHECKING from sphinx.transforms.post_transforms.images import ImageConverter -if False: - # For type annotation - from typing import Any, Dict # NoQA +if TYPE_CHECKING: + from sphinx.application import Sphinx + from sphinx.util.typing import ExtensionMetadata - from sphinx.application import Sphinx # NoQA class MyConverter(ImageConverter): conversion_rules = [ ('image/svg+xml', 'application/pdf'), ] - def is_available(self): - # type: () -> bool + def is_available(self) -> bool: return True - def convert(self, _from, _to): - # type: (unicode, unicode) -> bool + def convert(self, _from: str, _to: str) -> bool: """Mock converts the image from SVG to PDF.""" shutil.copyfile(_from, _to) return True -def setup(app): - # type: (Sphinx) -> Dict[unicode, Any] +def setup(app: Sphinx) -> ExtensionMetadata: app.add_post_transform(MyConverter) return { diff --git a/tests/test_application.py b/tests/test_application.py index d30a9b12463..f8676ddfedf 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -49,6 +49,7 @@ def test_instantiation( assert isinstance(app_, sphinx.application.Sphinx) +@pytest.mark.sphinx('html', testroot='root') def test_events(app): def empty(): pass @@ -75,24 +76,27 @@ def mock_callback(a_app, *args): assert app.emit('my_event', *emit_args) == [], 'Callback called when disconnected' +@pytest.mark.sphinx('html', testroot='root') def test_emit_with_nonascii_name_node(app): node = nodes.section(names=['\u65e5\u672c\u8a9e']) app.emit('my_event', node) +@pytest.mark.sphinx('html', testroot='root') def test_extensions(app): app.setup_extension('shutil') warning = strip_colors(app.warning.getvalue()) assert "extension 'shutil' has no setup() function" in warning +@pytest.mark.sphinx('html', testroot='root') def test_extension_in_blacklist(app): app.setup_extension('sphinxjp.themecore') msg = strip_colors(app.warning.getvalue()) assert msg.startswith("WARNING: the extension 'sphinxjp.themecore' was") -@pytest.mark.sphinx(testroot='add_source_parser') +@pytest.mark.sphinx('html', testroot='add_source_parser') def test_add_source_parser(app): assert set(app.config.source_suffix) == {'.rst', '.test'} @@ -106,7 +110,7 @@ def test_add_source_parser(app): assert app.registry.get_source_parsers()['test'].__name__ == 'TestSourceParser' -@pytest.mark.sphinx(testroot='extensions') +@pytest.mark.sphinx('html', testroot='extensions') def test_add_is_parallel_allowed(app): logging.setup(app, app.status, app.warning) diff --git a/tests/test_builders/test_build_epub.py b/tests/test_builders/test_build_epub.py index 178b46ff0a2..2dbdacf852b 100644 --- a/tests/test_builders/test_build_epub.py +++ b/tests/test_builders/test_build_epub.py @@ -2,9 +2,9 @@ import os import subprocess +import xml.etree.ElementTree as ET from pathlib import Path from subprocess import CalledProcessError -from xml.etree import ElementTree import pytest @@ -37,7 +37,7 @@ def __init__(self, tree): @classmethod def fromstring(cls, string): - tree = ElementTree.fromstring(string) # NoQA: S314 # using known data in tests + tree = ET.fromstring(string) # NoQA: S314 # using known data in tests return cls(tree) def find(self, match): @@ -443,7 +443,7 @@ def test_duplicated_toctree_entry(app): 'DO_EPUBCHECK' not in os.environ, reason='Skipped because DO_EPUBCHECK is not set', ) -@pytest.mark.sphinx('epub') +@pytest.mark.sphinx('epub', testroot='root') def test_run_epubcheck(app): app.build() diff --git a/tests/test_builders/test_build_gettext.py b/tests/test_builders/test_build_gettext.py index 905875fe1ef..169cc3bf438 100644 --- a/tests/test_builders/test_build_gettext.py +++ b/tests/test_builders/test_build_gettext.py @@ -47,6 +47,7 @@ def test_Catalog_duplicated_message(): @pytest.mark.sphinx( 'gettext', + testroot='root', srcdir='root-gettext', ) def test_build_gettext(app): @@ -66,6 +67,7 @@ def test_build_gettext(app): @pytest.mark.sphinx( 'gettext', + testroot='root', srcdir='root-gettext', ) def test_msgfmt(app): @@ -212,6 +214,7 @@ def test_gettext_custom_output_template(app): @pytest.mark.sphinx( 'gettext', + testroot='root', srcdir='root-gettext', confoverrides={'gettext_compact': 'documentation'}, ) @@ -283,6 +286,7 @@ def test_gettext_prolog_epilog_substitution_excluded(app): @pytest.mark.sphinx( 'gettext', + testroot='root', srcdir='gettext', confoverrides={ 'gettext_compact': False, diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index ecaab1651a1..2c4601849c9 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -124,7 +124,7 @@ def test_html4_error(make_app, tmp_path): ), ], ) -@pytest.mark.sphinx('html') +@pytest.mark.sphinx('html', testroot='root') @pytest.mark.test_params(shared_result='test_build_html_output_docutils18') def test_docutils_output(app, cached_etree_parse, fname, path, check): app.build() @@ -133,6 +133,7 @@ def test_docutils_output(app, cached_etree_parse, fname, path, check): @pytest.mark.sphinx( 'html', + testroot='root', parallel=2, ) def test_html_parallel(app): @@ -366,15 +367,12 @@ def test_html_style(app): @pytest.mark.sphinx( 'html', testroot='basic', - # alabaster changed default sidebars in 1.0.0 confoverrides={ 'html_sidebars': { '**': [ - 'about.html', - 'navigation.html', - 'relations.html', - 'searchbox.html', - 'donate.html', + 'localtoc.html', + 'searchfield.html', + 'sourcelink.html', ] } }, @@ -385,45 +383,58 @@ def test_html_sidebar(app): # default for alabaster app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') + # layout.html assert '