diff --git a/.ruff.toml b/.ruff.toml index 608c811da7c..a511d1c242d 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -7,7 +7,7 @@ extend-exclude = [ "tests/js/roots/*", "build/*", "doc/_build/*", - "sphinx/search/*", +# "sphinx/search/*", "doc/usage/extensions/example*.py", ] @@ -411,6 +411,8 @@ select = [ "sphinx/ext/autodoc/importer.py" = ["D402"] "sphinx/util/requests.py" = ["D402"] +"sphinx/search/*" = ["E501"] + "tests/*" = [ "E501", "ANN", # tests don't need annotations @@ -442,15 +444,11 @@ forced-separate = [ preview = true quote-style = "single" exclude = [ - "sphinx/_cli/*", "sphinx/addnodes.py", "sphinx/application.py", "sphinx/builders/*", - "sphinx/cmd/*", "sphinx/config.py", - "sphinx/directives/*", "sphinx/domains/*", - "sphinx/environment/*", "sphinx/ext/autodoc/__init__.py", "sphinx/ext/autodoc/directive.py", "sphinx/ext/autodoc/importer.py", @@ -470,19 +468,12 @@ exclude = [ "sphinx/ext/imgconverter.py", "sphinx/ext/imgmath.py", "sphinx/ext/inheritance_diagram.py", - "sphinx/ext/intersphinx/*", "sphinx/ext/linkcode.py", "sphinx/ext/mathjax.py", "sphinx/ext/napoleon/__init__.py", "sphinx/ext/napoleon/docstring.py", "sphinx/ext/todo.py", "sphinx/ext/viewcode.py", - "sphinx/pycode/*", - "sphinx/pygments_styles.py", "sphinx/registry.py", - "sphinx/search/*", - "sphinx/testing/*", - "sphinx/transforms/*", - "sphinx/util/*", "sphinx/writers/*", ] diff --git a/CHANGES.rst b/CHANGES.rst index c1d0f410acb..f0c3246d070 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -53,6 +53,12 @@ Features added GMT (universal time) instead of local time for the date-time supplied to :confval:`html_last_updated_fmt`. Patch by Adam Turner. +* #12910: Copyright entries now support the ``'%Y'`` placeholder + to substitute the current year. + This is helpful for reducing the reliance on Python modules + such as :py:mod:`time` or :py:mod:`datetime` in :file:`conf.py`. + See :ref:`the docs ` for further detail. + Patch by Adam Turner. Bugs fixed ---------- @@ -110,6 +116,10 @@ Bugs fixed * #12916: Restore support for custom templates named with the legacy ``_t`` suffix during ``apidoc`` RST rendering (regression in 7.4.0). Patch by James Addison. +* #12451: Only substitute copyright notice years with values from + ``SOURCE_DATE_EPOCH`` for entries that match the current system clock year, + and disallow substitution of future years. + Patch by James Addison and Adam Turner. Testing ------- diff --git a/doc/conf.py b/doc/conf.py index 6980eb91595..f4a6d9948eb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,7 +3,6 @@ import os import re -import time from typing import TYPE_CHECKING from sphinx import __display_version__ @@ -27,7 +26,7 @@ exclude_patterns = ['_build'] project = 'Sphinx' -copyright = f'2007-{time.strftime("%Y")}, the Sphinx developers' +copyright = '2007-%Y, the Sphinx developers' release = version = __display_version__ show_authors = True nitpicky = True diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index be6edee7d7c..d80d40a8265 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -114,6 +114,8 @@ Project information author = 'Joe Bloggs' +.. _config-copyright: + .. confval:: copyright project_copyright :type: :code-py:`str | Sequence[str]` @@ -128,6 +130,14 @@ Project information * :code-py:`copyright = 'YYYY-YYYY, Author Name'` * :code-py:`copyright = 'YYYY-YYYY Author Name'` + If the string :code-py:`'%Y'` appears in a copyright line, + it will be replaced with the current four-digit year. + For example: + + * :code-py:`copyright = '%Y'` + * :code-py:`copyright = '%Y, Author Name'` + * :code-py:`copyright = 'YYYY-%Y, Author Name'` + .. versionadded:: 3.5 The :code-py:`project_copyright` alias. @@ -135,6 +145,9 @@ Project information The value may now be a sequence of copyright statements in the above form, which will be displayed each to their own line. + .. versionchanged:: 8.1 + Copyright statements support the :code-py:`'%Y'` placeholder. + .. confval:: version :type: :code-py:`str` :default: :code-py:`''` @@ -2528,6 +2541,7 @@ so the HTML options also apply where appropriate. :default: The value of **copyright** The copyright of the document. + See :confval:`copyright` for permitted formats. .. confval:: epub_identifier :type: :code-py:`str` diff --git a/doc/usage/domains/index.rst b/doc/usage/domains/index.rst index 8c47134159f..eda2da3f652 100644 --- a/doc/usage/domains/index.rst +++ b/doc/usage/domains/index.rst @@ -158,19 +158,22 @@ giving the domain name, i.e. :: Cross-referencing syntax ~~~~~~~~~~~~~~~~~~~~~~~~ -For cross-reference roles provided by domains, the same facilities exist as for -general cross-references. See :ref:`xref-syntax`. - +For cross-reference roles provided by domains, +the same :ref:`cross-referencing modifiers ` exist +as for general cross-references. In short: -* You may supply an explicit title and reference target: ``:role:`title - ``` will refer to *target*, but the link text will be *title*. +* You may supply an explicit title and reference target: + ``:py:mod:`mathematical functions ``` will refer to the ``math`` module, + but the link text will be "mathematical functions". -* If you prefix the content with ``!``, no reference/hyperlink will be created. +* If you prefix the content with an exclamation mark (``!``), + no reference/hyperlink will be created. * If you prefix the content with ``~``, the link text will only be the last - component of the target. For example, ``:py:meth:`~Queue.Queue.get``` will - refer to ``Queue.Queue.get`` but only display ``get`` as the link text. + component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. Built-in domains ---------------- diff --git a/doc/usage/domains/python.rst b/doc/usage/domains/python.rst index 5b98baa78b0..2ff27c4bb3d 100644 --- a/doc/usage/domains/python.rst +++ b/doc/usage/domains/python.rst @@ -716,18 +716,45 @@ a matching identifier is found: .. versionadded:: 0.4 -The name enclosed in this markup can include a module name and/or a class name. -For example, ``:py:func:`filter``` could refer to a function named ``filter`` -in the current module, or the built-in function of that name. In contrast, -``:py:func:`foo.filter``` clearly refers to the ``filter`` function in the -``foo`` module. - -Normally, names in these roles are searched first without any further -qualification, then with the current module name prepended, then with the -current module and class name (if any) prepended. If you prefix the name with -a dot, this order is reversed. For example, in the documentation of Python's -:mod:`codecs` module, ``:py:func:`open``` always refers to the built-in -function, while ``:py:func:`.open``` refers to :func:`codecs.open`. + +Target specification +^^^^^^^^^^^^^^^^^^^^ + +The target can be specified as a fully qualified name +(e.g. ``:py:meth:`my_module.MyClass.my_method```) +or any shortened version +(e.g. ``:py:meth:`MyClass.my_method``` or ``:py:meth:`my_method```). +See `target resolution`_ for details on the resolution of shortened names. + +:ref:`Cross-referencing modifiers ` can be applied. +In short: + +* You may supply an explicit title and reference target: + ``:py:mod:`mathematical functions ``` will refer to the ``math`` module, + but the link text will be "mathematical functions". + +* If you prefix the content with an exclamation mark (``!``), + no reference/hyperlink will be created. + +* If you prefix the content with ``~``, the link text will only be the last + component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. + + +Target resolution +^^^^^^^^^^^^^^^^^ + +A given link target name is resolved to an object using the following strategy: + +Names in these roles are searched first without any further qualification, +then with the current module name prepended, +then with the current module and class name (if any) prepended. + +If you prefix the name with a dot (``.``), this order is reversed. +For example, in the documentation of Python's :py:mod:`codecs` module, +``:py:func:`open``` always refers to the built-in function, +while ``:py:func:`.open``` refers to :func:`codecs.open`. A similar heuristic is used to determine whether the name is an attribute of the currently documented class. diff --git a/doc/usage/referencing.rst b/doc/usage/referencing.rst index 57611660f10..6c18cf80ea8 100644 --- a/doc/usage/referencing.rst +++ b/doc/usage/referencing.rst @@ -4,83 +4,64 @@ Cross-referencing syntax ======================== -Cross-references are generated by many semantic interpreted text roles. -Basically, you only need to write ``:role:`target```, and a link will be -created to the item named *target* of the type indicated by *role*. The link's -text will be the same as *target*. +One of Sphinx's most useful features is creating automatic cross-references +through semantic cross-referencing roles. +A cross reference to an object description, such as ``:func:`spam```, +will create a link to the place where ``spam()`` is documented, +appropriate to each output format (HTML, PDF, ePUB, etc.). -There are some additional facilities, however, that make cross-referencing -roles more versatile: +Sphinx supports various cross-referencing roles to create links +to other elements in the documentation. +In general, writing ``:role:`target``` creates a link to +the object called *target* of the type indicated by *role*. +The link's text depends the role but is often the same as or similar to *target*. -* You may supply an explicit title and reference target, - like in reStructuredText direct hyperlinks: - ``:role:`title ``` will refer to *target*, - but the link text will be *title*. +.. _xref-modifiers: -* If you prefix the content with ``!``, no reference/hyperlink will be created. +The behavior can be modified in the following ways: -* If you prefix the content with ``~``, the link text will only be the last - component of the target. For example, ``:py:meth:`~Queue.Queue.get``` will - refer to ``Queue.Queue.get`` but only display ``get`` as the link text. This - does not work with all cross-reference roles, but is domain specific. +* **Custom link text:** + You can specify the link text explicitly using the same + notation as in reStructuredText :ref:`external links `: + ``:role:`custom text ``` will refer to *target* + and display *custom text* as the text of the link. - In HTML output, the link's ``title`` attribute (that is e.g. shown as a - tool-tip on mouse-hover) will always be the full target name. +* **Suppressed link:** + Prefixing with an exclamation mark (``!``) prevents the creation of a link + but otherwise keeps the visual output of the role. + For example, writing ``:py:func:`!target``` displays :py:func:`!target`, + with no link generated. -.. _any-role: - -Cross-referencing anything --------------------------- - -.. rst:role:: any - - .. versionadded:: 1.3 - - This convenience role tries to do its best to find a valid target for its - reference text. + This is helpful for cases in which the link target does not exist; + e.g. changelog entries that describe removed functionality, + or third-party libraries that don't support :doc:`intersphinx + `. + Suppressing the link prevents warnings in :confval:`nitpicky` mode. - * First, it tries standard cross-reference targets that would be referenced - by :rst:role:`doc`, :rst:role:`ref` or :rst:role:`option`. +* **Modified domain reference:** + When :ref:`referencing domain objects `, + a tilde ``~`` prefix shortens the link text to the last component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. - Custom objects added to the standard domain by extensions (see - :meth:`.Sphinx.add_object_type`) are also searched. + In HTML output, the link's ``title`` attribute + (that is e.g. shown as a tool-tip on mouse-hover) + will always be the full target name. - * Then, it looks for objects (targets) in all loaded domains. It is up to - the domains how specific a match must be. For example, in the Python - domain a reference of ``:any:`Builder``` would match the - ``sphinx.builders.Builder`` class. - If none or multiple targets are found, a warning will be emitted. In the - case of multiple targets, you can change "any" to a specific role. - - This role is a good candidate for setting :confval:`default_role`. If you - do, you can write cross-references without a lot of markup overhead. For - example, in this Python function documentation:: - - .. function:: install() - - This function installs a `handler` for every signal known by the - `signal` module. See the section `about-signals` for more information. - - there could be references to a glossary term (usually ``:term:`handler```), a - Python module (usually ``:py:mod:`signal``` or ``:mod:`signal```) and a - section (usually ``:ref:`about-signals```). - - The :rst:role:`any` role also works together with the - :mod:`~sphinx.ext.intersphinx` extension: when no local cross-reference is - found, all object types of intersphinx inventories are also searched. +.. _ref-objects: Cross-referencing objects ------------------------- These roles are described with their respective domains: -* :ref:`Python ` * :ref:`C ` * :ref:`C++ ` * :ref:`JavaScript ` * :ref:`reStructuredText ` +* :ref:`Python ` .. _ref-role: @@ -267,3 +248,48 @@ The following role creates a cross-reference to a term in a If you use a term that's not explained in a glossary, you'll get a warning during build. + + +.. _any-role: + +Cross-referencing anything +-------------------------- + +.. rst:role:: any + + .. versionadded:: 1.3 + + This convenience role tries to do its best to find a valid target for its + reference text. + + * First, it tries standard cross-reference targets that would be referenced + by :rst:role:`doc`, :rst:role:`ref` or :rst:role:`option`. + + Custom objects added to the standard domain by extensions (see + :meth:`.Sphinx.add_object_type`) are also searched. + + * Then, it looks for objects (targets) in all loaded domains. It is up to + the domains how specific a match must be. For example, in the Python + domain a reference of ``:any:`Builder``` would match the + ``sphinx.builders.Builder`` class. + + If none or multiple targets are found, a warning will be emitted. In the + case of multiple targets, you can change "any" to a specific role. + + This role is a good candidate for setting :confval:`default_role`. If you + do, you can write cross-references without a lot of markup overhead. For + example, in this Python function documentation:: + + .. function:: install() + + This function installs a `handler` for every signal known by the + `signal` module. See the section `about-signals` for more information. + + there could be references to a glossary term (usually ``:term:`handler```), a + Python module (usually ``:py:mod:`signal``` or ``:mod:`signal```) and a + section (usually ``:ref:`about-signals```). + + The :rst:role:`any` role also works together with the + :mod:`~sphinx.ext.intersphinx` extension: when no local cross-reference is + found, all object types of intersphinx inventories are also searched. + diff --git a/doc/usage/restructuredtext/basics.rst b/doc/usage/restructuredtext/basics.rst index 53547486ac8..5d60ea81de4 100644 --- a/doc/usage/restructuredtext/basics.rst +++ b/doc/usage/restructuredtext/basics.rst @@ -203,6 +203,8 @@ Two more syntaxes are supported: *CSV tables* and *List tables*. They use an Hyperlinks ---------- +.. _rst-external-links: + External links ~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 8b13be94ebf..cbc4beaad58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ lint = [ "sphinx-lint>=0.9", "types-colorama==0.4.15.20240311", "types-defusedxml==0.7.0.20240218", - "types-docutils==0.21.0.20240724", + "types-docutils==0.21.0.20241004", "types-Pillow==10.2.0.20240822", "types-Pygments==2.18.0.20240506", "types-requests==2.32.0.20240914", # align with requests @@ -146,7 +146,6 @@ files = [ exclude = [ "tests/roots", # tests/ - "^tests/test_events\\.py$", "^tests/test_quickstart\\.py$", "^tests/test_search\\.py$", # tests/test_builders @@ -175,14 +174,12 @@ exclude = [ # tests/test_extensions "^tests/test_extensions/test_ext_apidoc\\.py$", "^tests/test_extensions/test_ext_autodoc\\.py$", - "^tests/test_extensions/test_ext_autodoc_autofunction\\.py$", "^tests/test_extensions/test_ext_autodoc_events\\.py$", "^tests/test_extensions/test_ext_autodoc_mock\\.py$", "^tests/test_extensions/test_ext_autosummary\\.py$", "^tests/test_extensions/test_ext_doctest\\.py$", "^tests/test_extensions/test_ext_inheritance_diagram\\.py$", "^tests/test_extensions/test_ext_intersphinx\\.py$", - "^tests/test_extensions/test_ext_napoleon\\.py$", "^tests/test_extensions/test_ext_napoleon_docstring\\.py$", # tests/test_intl "^tests/test_intl/test_intl\\.py$", diff --git a/sphinx/_cli/__init__.py b/sphinx/_cli/__init__.py index 1e9c9314738..3160f08373c 100644 --- a/sphinx/_cli/__init__.py +++ b/sphinx/_cli/__init__.py @@ -38,7 +38,9 @@ from collections.abc import Callable, Iterable, Iterator, Sequence from typing import NoReturn, TypeAlias - _PARSER_SETUP: TypeAlias = Callable[[argparse.ArgumentParser], argparse.ArgumentParser] + _PARSER_SETUP: TypeAlias = Callable[ + [argparse.ArgumentParser], argparse.ArgumentParser + ] _RUNNER: TypeAlias = Callable[[argparse.Namespace], int] from typing import Protocol @@ -50,8 +52,7 @@ class _SubcommandModule(Protocol): # Map of command name to import path. -_COMMANDS: dict[str, str] = { -} +_COMMANDS: dict[str, str] = {} def _load_subcommand_descriptions() -> Iterator[tuple[str, str]]: @@ -61,7 +62,7 @@ def _load_subcommand_descriptions() -> Iterator[tuple[str, str]]: description = module.parser_description except AttributeError: # log an error here, but don't fail the full enumeration - print(f"Failed to load the description for {command}", file=sys.stderr) + print(f'Failed to load the description for {command}', file=sys.stderr) else: yield command, description.split('\n\n', 1)[0] @@ -79,7 +80,8 @@ def format_help(self) -> str: ] if commands := list(_load_subcommand_descriptions()): - command_max_length = min(max(map(len, next(zip(*commands, strict=True), ()))), 22) + command_lengths = map(len, next(zip(*commands, strict=True), ())) + command_max_length = min(max(command_lengths), 22) help_fragments += [ '\n', bold(underline(__('Commands:'))), @@ -95,8 +97,11 @@ def format_help(self) -> str: # Uppercase the title of the Optionals group self._optionals.title = __('Options') for argument_group in self._action_groups[1:]: - if arguments := [action for action in argument_group._group_actions - if action.help != argparse.SUPPRESS]: + if arguments := [ + action + for action in argument_group._group_actions + if action.help != argparse.SUPPRESS + ]: help_fragments += self._format_optional_arguments( arguments, argument_group.title or '', @@ -104,7 +109,9 @@ def format_help(self) -> str: help_fragments += [ '\n', - __('For more information, visit https://www.sphinx-doc.org/en/master/man/.'), + __( + 'For more information, visit https://www.sphinx-doc.org/en/master/man/.' + ), '\n', ] return ''.join(help_fragments) @@ -123,7 +130,7 @@ def _format_optional_arguments( opt = prefix + ' ' + ', '.join(map(bold, action.option_strings)) if action.nargs != 0: opt += ' ' + self._format_metavar( - action.nargs, action.metavar, action.choices, action.dest, + action.nargs, action.metavar, action.choices, action.dest ) yield opt yield '\n' @@ -161,10 +168,11 @@ def _format_metavar( raise ValueError(msg) def error(self, message: str) -> NoReturn: - sys.stderr.write(__( - '{0}: error: {1}\n' - "Run '{0} --help' for information" # NoQA: COM812 - ).format(self.prog, message)) + sys.stderr.write( + __( + '{0}: error: {1}\n' "Run '{0} --help' for information" # NoQA: COM812 + ).format(self.prog, message) + ) raise SystemExit(2) @@ -172,18 +180,23 @@ def _create_parser() -> _RootArgumentParser: parser = _RootArgumentParser( prog='sphinx', description=__(' Manage documentation with Sphinx.'), - epilog=__('For more information, visit https://www.sphinx-doc.org/en/master/man/.'), + epilog=__( + 'For more information, visit https://www.sphinx-doc.org/en/master/man/.' + ), add_help=False, allow_abbrev=False, ) parser.add_argument( - '-V', '--version', + '-V', + '--version', action='store_true', default=argparse.SUPPRESS, help=__('Show the version and exit.'), ) parser.add_argument( - '-h', '-?', '--help', + '-h', + '-?', + '--help', action='store_true', default=argparse.SUPPRESS, help=__('Show this message and exit.'), @@ -192,14 +205,16 @@ def _create_parser() -> _RootArgumentParser: # logging control log_control = parser.add_argument_group(__('Logging')) log_control.add_argument( - '-v', '--verbose', + '-v', + '--verbose', action='count', dest='verbosity', default=0, help=__('Increase verbosity (can be repeated)'), ) log_control.add_argument( - '-q', '--quiet', + '-q', + '--quiet', action='store_const', dest='verbosity', const=-1, @@ -235,6 +250,7 @@ def _parse_command(argv: Sequence[str] = ()) -> tuple[str, Sequence[str]]: # Handle '--version' or '-V' passed to the main command or any subcommand if 'version' in args or {'-V', '--version'}.intersection(command_argv): from sphinx import __display_version__ + sys.stderr.write(f'sphinx {__display_version__}\n') raise SystemExit(0) @@ -245,8 +261,12 @@ def _parse_command(argv: Sequence[str] = ()) -> tuple[str, Sequence[str]]: raise SystemExit(0) if command_name not in _COMMANDS: - sys.stderr.write(__(f'sphinx: {command_name!r} is not a sphinx command. ' - "See 'sphinx --help'.\n")) + sys.stderr.write( + __( + f'sphinx: {command_name!r} is not a sphinx command. ' + "See 'sphinx --help'.\n" + ) + ) raise SystemExit(2) return command_name, command_argv diff --git a/sphinx/_cli/util/colour.py b/sphinx/_cli/util/colour.py index a89d04ec52b..a93d7d044a1 100644 --- a/sphinx/_cli/util/colour.py +++ b/sphinx/_cli/util/colour.py @@ -57,6 +57,7 @@ def inner(text: str) -> str: if _COLOURING_DISABLED: return text return f'\x1b[{escape_code}m{text}\x1b[39;49;00m' + return inner @@ -69,11 +70,13 @@ def inner(text: str) -> str: if sys.platform == 'win32': _create_input_mode_colour_func = _create_colour_func else: + def _create_input_mode_colour_func(escape_code: str, /) -> Callable[[str], str]: def inner(text: str) -> str: if _COLOURING_DISABLED: return text return f'\x01\x1b[{escape_code}m\x02{text}\x01\x1b[39;49;00m\x02' + return inner diff --git a/sphinx/_cli/util/errors.py b/sphinx/_cli/util/errors.py index dac0fb83c56..e630df22b6c 100644 --- a/sphinx/_cli/util/errors.py +++ b/sphinx/_cli/util/errors.py @@ -72,10 +72,15 @@ def save_traceback(app: Sphinx | None, exc: BaseException) -> str: if app is not None: extensions = app.extensions.values() last_msgs = '\n'.join(f'* {strip_colors(s)}' for s in app.messagelog) - exts_list = '\n'.join(f'* {ext.name} ({ext.version})' for ext in extensions - if ext.version != 'builtin') - - with tempfile.NamedTemporaryFile(suffix='.log', prefix='sphinx-err-', delete=False) as f: + exts_list = '\n'.join( + f'* {ext.name} ({ext.version})' + for ext in extensions + if ext.version != 'builtin' + ) + + with tempfile.NamedTemporaryFile( + suffix='.log', prefix='sphinx-err-', delete=False + ) as f: f.write(error_info(last_msgs, exts_list, exc_format).encode('utf-8')) return f.name @@ -143,9 +148,13 @@ def print_red(*values: str) -> None: print_red(__('Recursion error:')) print_err(str(exception)) print_err() - print_err(__('This can happen with very large or deeply nested source ' - 'files. You can carefully increase the default Python ' - 'recursion limit of 1000 in conf.py with e.g.:')) + print_err( + __( + 'This can happen with very large or deeply nested source ' + 'files. You can carefully increase the default Python ' + 'recursion limit of 1000 in conf.py with e.g.:' + ) + ) print_err('\n import sys\n sys.setrecursionlimit(1_500)\n') return @@ -159,7 +168,15 @@ def print_red(*values: str) -> None: print_err(__('The full traceback has been saved in:')) print_err(traceback_info_path) print_err() - print_err(__('To report this error to the developers, please open an issue ' - 'at . Thanks!')) - print_err(__('Please also report this if it was a user error, so ' - 'that a better error message can be provided next time.')) + print_err( + __( + 'To report this error to the developers, please open an issue ' + 'at . Thanks!' + ) + ) + print_err( + __( + 'Please also report this if it was a user error, so ' + 'that a better error message can be provided next time.' + ) + ) diff --git a/sphinx/builders/html/_build_info.py b/sphinx/builders/html/_build_info.py index 5b364c0d9fc..281ad5ab977 100644 --- a/sphinx/builders/html/_build_info.py +++ b/sphinx/builders/html/_build_info.py @@ -2,16 +2,14 @@ from __future__ import annotations -import hashlib -import types from typing import TYPE_CHECKING from sphinx.locale import __ +from sphinx.util._serialise import stable_hash if TYPE_CHECKING: from collections.abc import Set from pathlib import Path - from typing import Any from sphinx.config import Config, _ConfigRebuild from sphinx.util.tags import Tags @@ -57,10 +55,10 @@ def __init__( if config: values = {c.name: c.value for c in config.filter(config_categories)} - self.config_hash = _stable_hash(values) + self.config_hash = stable_hash(values) if tags: - self.tags_hash = _stable_hash(sorted(tags)) + self.tags_hash = stable_hash(sorted(tags)) def __eq__(self, other: BuildInfo) -> bool: # type: ignore[override] return (self.config_hash == other.config_hash and @@ -75,20 +73,3 @@ def dump(self, filename: Path, /) -> None: f'tags: {self.tags_hash}\n' ) filename.write_text(build_info, encoding="utf-8") - - -def _stable_hash(obj: Any) -> str: - """Return a stable hash for a Python data structure. - - We can't just use the md5 of str(obj) as the order of collections - may be random. - """ - if isinstance(obj, dict): - obj = sorted(map(_stable_hash, obj.items())) - if isinstance(obj, list | tuple | set | frozenset): - obj = sorted(map(_stable_hash, obj)) - elif isinstance(obj, type | types.FunctionType): - # The default repr() of functions includes the ID, which is not ideal. - # We use the fully qualified name instead. - obj = f'{obj.__module__}.{obj.__qualname__}' - return hashlib.md5(str(obj).encode(), usedforsecurity=False).hexdigest() diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index 9f6cf2a3363..3c3d8e4b4de 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -32,19 +32,23 @@ from typing import Protocol class SupportsWrite(Protocol): - def write(self, text: str, /) -> int | None: - ... + def write(self, text: str, /) -> int | None: ... # NoQA: E704 def handle_exception( - app: Sphinx | None, args: Any, exception: BaseException, stderr: TextIO = sys.stderr, + app: Sphinx | None, + args: Any, + exception: BaseException, + stderr: TextIO = sys.stderr, ) -> None: if isinstance(exception, bdb.BdbQuit): return if args.pdb: - print(red(__('Exception occurred while building, starting debugger:')), - file=stderr) + print( + red(__('Exception occurred while building, starting debugger:')), + file=stderr, + ) traceback.print_exc() pdb.post_mortem(sys.exc_info()[2]) else: @@ -69,30 +73,60 @@ def handle_exception( print(red(__('Encoding error:')), file=stderr) print(terminal_safe(str(exception)), file=stderr) tbpath = save_traceback(app, exception) - print(red(__('The full traceback has been saved in %s, if you want ' - 'to report the issue to the developers.') % tbpath), - file=stderr) - elif isinstance(exception, RuntimeError) and 'recursion depth' in str(exception): + print( + red( + __( + 'The full traceback has been saved in %s, if you want ' + 'to report the issue to the developers.' + ) + % tbpath + ), + file=stderr, + ) + elif ( + isinstance(exception, RuntimeError) + and 'recursion depth' in str(exception) + ): # fmt: skip print(red(__('Recursion error:')), file=stderr) print(terminal_safe(str(exception)), file=stderr) print(file=stderr) - print(__('This can happen with very large or deeply nested source ' - 'files. You can carefully increase the default Python ' - 'recursion limit of 1000 in conf.py with e.g.:'), file=stderr) + print( + __( + 'This can happen with very large or deeply nested source ' + 'files. You can carefully increase the default Python ' + 'recursion limit of 1000 in conf.py with e.g.:' + ), + file=stderr, + ) print(' import sys; sys.setrecursionlimit(1500)', file=stderr) else: print(red(__('Exception occurred:')), file=stderr) print(format_exception_cut_frames().rstrip(), file=stderr) tbpath = save_traceback(app, exception) - print(red(__('The full traceback has been saved in %s, if you ' - 'want to report the issue to the developers.') % tbpath), - file=stderr) - print(__('Please also report this if it was a user error, so ' - 'that a better error message can be provided next time.'), - file=stderr) - print(__('A bug report can be filed in the tracker at ' - '. Thanks!'), - file=stderr) + print( + red( + __( + 'The full traceback has been saved in %s, if you ' + 'want to report the issue to the developers.' + ) + % tbpath + ), + file=stderr, + ) + print( + __( + 'Please also report this if it was a user error, so ' + 'that a better error message can be provided next time.' + ), + file=stderr, + ) + print( + __( + 'A bug report can be filed in the tracker at ' + '. Thanks!' + ), + file=stderr, + ) def jobs_argument(value: str) -> int: @@ -106,7 +140,9 @@ def jobs_argument(value: str) -> int: else: jobs = int(value) if jobs <= 0: - raise argparse.ArgumentTypeError(__('job number should be a positive number')) + raise argparse.ArgumentTypeError( + __('job number should be a positive number') + ) else: return jobs @@ -130,85 +166,203 @@ def get_parser() -> argparse.ArgumentParser: By default, everything that is outdated is built. Output only for selected files can be built by specifying individual filenames. -""")) - - parser.add_argument('--version', action='version', dest='show_version', - version=f'%(prog)s {__display_version__}') - - parser.add_argument('sourcedir', metavar='SOURCE_DIR', - help=__('path to documentation source files')) - parser.add_argument('outputdir', metavar='OUTPUT_DIR', - help=__('path to output directory')) - parser.add_argument('filenames', nargs='*', - help=__('(optional) a list of specific files to rebuild. ' - 'Ignored if --write-all is specified')) +"""), + ) + + parser.add_argument( + '--version', + action='version', + dest='show_version', + version=f'%(prog)s {__display_version__}', + ) + + parser.add_argument( + 'sourcedir', metavar='SOURCE_DIR', help=__('path to documentation source files') + ) + parser.add_argument( + 'outputdir', metavar='OUTPUT_DIR', help=__('path to output directory') + ) + parser.add_argument( + 'filenames', + nargs='*', + help=__( + '(optional) a list of specific files to rebuild. ' + 'Ignored if --write-all is specified' + ), + ) group = parser.add_argument_group(__('general options')) - group.add_argument('--builder', '-b', metavar='BUILDER', dest='builder', - default='html', - help=__("builder to use (default: 'html')")) - group.add_argument('--jobs', '-j', metavar='N', default=1, type=jobs_argument, - dest='jobs', - help=__('run in parallel with N processes, when possible. ' - "'auto' uses the number of CPU cores")) - group.add_argument('--write-all', '-a', action='store_true', dest='force_all', - help=__('write all files (default: only write new and ' - 'changed files)')) - group.add_argument('--fresh-env', '-E', action='store_true', dest='freshenv', - help=__("don't use a saved environment, always read " - 'all files')) + group.add_argument( + '--builder', + '-b', + metavar='BUILDER', + dest='builder', + default='html', + help=__("builder to use (default: 'html')"), + ) + group.add_argument( + '--jobs', + '-j', + metavar='N', + default=1, + type=jobs_argument, + dest='jobs', + help=__( + 'run in parallel with N processes, when possible. ' + "'auto' uses the number of CPU cores" + ), + ) + group.add_argument( + '--write-all', + '-a', + action='store_true', + dest='force_all', + help=__('write all files (default: only write new and ' 'changed files)'), + ) + group.add_argument( + '--fresh-env', + '-E', + action='store_true', + dest='freshenv', + help=__("don't use a saved environment, always read " 'all files'), + ) group = parser.add_argument_group(__('path options')) - group.add_argument('--doctree-dir', '-d', metavar='PATH', dest='doctreedir', - help=__('directory for doctree and environment files ' - '(default: OUTPUT_DIR/.doctrees)')) - group.add_argument('--conf-dir', '-c', metavar='PATH', dest='confdir', - help=__('directory for the configuration file (conf.py) ' - '(default: SOURCE_DIR)')) + group.add_argument( + '--doctree-dir', + '-d', + metavar='PATH', + dest='doctreedir', + help=__( + 'directory for doctree and environment files ' + '(default: OUTPUT_DIR/.doctrees)' + ), + ) + group.add_argument( + '--conf-dir', + '-c', + metavar='PATH', + dest='confdir', + help=__( + 'directory for the configuration file (conf.py) ' '(default: SOURCE_DIR)' + ), + ) group = parser.add_argument_group('build configuration options') - group.add_argument('--isolated', '-C', action='store_true', dest='noconfig', - help=__('use no configuration file, only use settings from -D options')) - group.add_argument('--define', '-D', metavar='setting=value', action='append', - dest='define', default=[], - help=__('override a setting in configuration file')) - group.add_argument('--html-define', '-A', metavar='name=value', action='append', - dest='htmldefine', default=[], - help=__('pass a value into HTML templates')) - group.add_argument('--tag', '-t', metavar='TAG', action='append', - dest='tags', default=[], - help=__('define tag: include "only" blocks with TAG')) - group.add_argument('--nitpicky', '-n', action='store_true', dest='nitpicky', - help=__('nitpicky mode: warn about all missing references')) + group.add_argument( + '--isolated', + '-C', + action='store_true', + dest='noconfig', + help=__('use no configuration file, only use settings from -D options'), + ) + group.add_argument( + '--define', + '-D', + metavar='setting=value', + action='append', + dest='define', + default=[], + help=__('override a setting in configuration file'), + ) + group.add_argument( + '--html-define', + '-A', + metavar='name=value', + action='append', + dest='htmldefine', + default=[], + help=__('pass a value into HTML templates'), + ) + group.add_argument( + '--tag', + '-t', + metavar='TAG', + action='append', + dest='tags', + default=[], + help=__('define tag: include "only" blocks with TAG'), + ) + group.add_argument( + '--nitpicky', + '-n', + action='store_true', + dest='nitpicky', + help=__('nitpicky mode: warn about all missing references'), + ) group = parser.add_argument_group(__('console output options')) - group.add_argument('--verbose', '-v', action='count', dest='verbosity', - default=0, - help=__('increase verbosity (can be repeated)')) - group.add_argument('--quiet', '-q', action='store_true', dest='quiet', - help=__('no output on stdout, just warnings on stderr')) - group.add_argument('--silent', '-Q', action='store_true', dest='really_quiet', - help=__('no output at all, not even warnings')) - group.add_argument('--color', action='store_const', dest='color', - const='yes', default='auto', - help=__('do emit colored output (default: auto-detect)')) - group.add_argument('--no-color', '-N', action='store_const', dest='color', - const='no', - help=__('do not emit colored output (default: auto-detect)')) + group.add_argument( + '--verbose', + '-v', + action='count', + dest='verbosity', + default=0, + help=__('increase verbosity (can be repeated)'), + ) + group.add_argument( + '--quiet', + '-q', + action='store_true', + dest='quiet', + help=__('no output on stdout, just warnings on stderr'), + ) + group.add_argument( + '--silent', + '-Q', + action='store_true', + dest='really_quiet', + help=__('no output at all, not even warnings'), + ) + group.add_argument( + '--color', + action='store_const', + dest='color', + const='yes', + default='auto', + help=__('do emit colored output (default: auto-detect)'), + ) + group.add_argument( + '--no-color', + '-N', + action='store_const', + dest='color', + const='no', + help=__('do not emit colored output (default: auto-detect)'), + ) group = parser.add_argument_group(__('warning control options')) - group.add_argument('--warning-file', '-w', metavar='FILE', dest='warnfile', - 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( + '--warning-file', + '-w', + metavar='FILE', + dest='warnfile', + 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', 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')) + 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' @@ -219,11 +373,13 @@ def get_parser() -> argparse.ArgumentParser: def make_main(argv: Sequence[str]) -> int: """Sphinx build "make mode" entry.""" from sphinx.cmd import make_mode + return make_mode.run_make_mode(argv[1:]) -def _parse_arguments(parser: argparse.ArgumentParser, - argv: Sequence[str]) -> argparse.Namespace: +def _parse_arguments( + parser: argparse.ArgumentParser, argv: Sequence[str] +) -> argparse.Namespace: args = parser.parse_args(argv) return args @@ -243,7 +399,9 @@ def _parse_doctreedir(doctreedir: str, outputdir: str) -> str: def _validate_filenames( - parser: argparse.ArgumentParser, force_all: bool, filenames: list[str], + parser: argparse.ArgumentParser, + force_all: bool, + filenames: list[str], ) -> None: if force_all and filenames: parser.error(__('cannot combine -a option and filenames')) @@ -276,10 +434,9 @@ def _parse_logging( warnfile = path.abspath(warnfile) ensuredir(path.dirname(warnfile)) # the caller is responsible for closing this file descriptor - warnfp = open(warnfile, 'w', encoding="utf-8") # NoQA: SIM115 + warnfp = open(warnfile, 'w', encoding='utf-8') # NoQA: SIM115 except Exception as exc: - parser.error(__('cannot open warning file %r: %s') % ( - warnfile, exc)) + parser.error(__('cannot open warning file %r: %s') % (warnfile, exc)) warning = TeeStripANSI(warning, warnfp) # type: ignore[assignment] error = warning @@ -326,23 +483,33 @@ def build_main(argv: Sequence[str]) -> int: _validate_filenames(parser, args.force_all, args.filenames) _validate_colour_support(args.color) args.status, args.warning, args.error, warnfp = _parse_logging( - parser, args.quiet, args.really_quiet, args.warnfile) + parser, args.quiet, args.really_quiet, args.warnfile + ) args.confoverrides = _parse_confoverrides( - parser, args.define, args.htmldefine, args.nitpicky) + parser, args.define, args.htmldefine, args.nitpicky + ) app = None try: confdir = args.confdir or args.sourcedir with patch_docutils(confdir), docutils_namespace(): 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, + 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, + 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 @@ -390,6 +557,7 @@ def main(argv: Sequence[str] = (), /) -> int: return _bug_report_info() if argv[:1] == ['-M']: 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 d1ba3fccf9c..b590c423e85 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -29,31 +29,34 @@ from collections.abc import Sequence BUILDERS = [ - ("", "html", "to make standalone HTML files"), - ("", "dirhtml", "to make HTML files named index.html in directories"), - ("", "singlehtml", "to make a single large HTML file"), - ("", "pickle", "to make pickle files"), - ("", "json", "to make JSON files"), - ("", "htmlhelp", "to make HTML files and an HTML help project"), - ("", "qthelp", "to make HTML files and a qthelp project"), - ("", "devhelp", "to make HTML files and a Devhelp project"), - ("", "epub", "to make an epub"), - ("", "latex", "to make LaTeX files, you can set PAPER=a4 or PAPER=letter"), - ("posix", "latexpdf", "to make LaTeX and PDF files (default pdflatex)"), - ("posix", "latexpdfja", "to make LaTeX files and run them through platex/dvipdfmx"), - ("", "text", "to make text files"), - ("", "man", "to make manual pages"), - ("", "texinfo", "to make Texinfo files"), - ("posix", "info", "to make Texinfo files and run them through makeinfo"), - ("", "gettext", "to make PO message catalogs"), - ("", "changes", "to make an overview of all changed/added/deprecated items"), - ("", "xml", "to make Docutils-native XML files"), - ("", "pseudoxml", "to make pseudoxml-XML files for display purposes"), - ("", "linkcheck", "to check all external links for integrity"), - ("", "doctest", "to run all doctests embedded in the documentation " - "(if enabled)"), - ("", "coverage", "to run coverage check of the documentation (if enabled)"), - ("", "clean", "to remove everything in the build directory"), + ('', 'html', 'to make standalone HTML files'), + ('', 'dirhtml', 'to make HTML files named index.html in directories'), + ('', 'singlehtml', 'to make a single large HTML file'), + ('', 'pickle', 'to make pickle files'), + ('', 'json', 'to make JSON files'), + ('', 'htmlhelp', 'to make HTML files and an HTML help project'), + ('', 'qthelp', 'to make HTML files and a qthelp project'), + ('', 'devhelp', 'to make HTML files and a Devhelp project'), + ('', 'epub', 'to make an epub'), + ('', 'latex', 'to make LaTeX files, you can set PAPER=a4 or PAPER=letter'), + ('posix', 'latexpdf', 'to make LaTeX and PDF files (default pdflatex)'), + ('posix', 'latexpdfja', 'to make LaTeX files and run them through platex/dvipdfmx'), + ('', 'text', 'to make text files'), + ('', 'man', 'to make manual pages'), + ('', 'texinfo', 'to make Texinfo files'), + ('posix', 'info', 'to make Texinfo files and run them through makeinfo'), + ('', 'gettext', 'to make PO message catalogs'), + ('', 'changes', 'to make an overview of all changed/added/deprecated items'), + ('', 'xml', 'to make Docutils-native XML files'), + ('', 'pseudoxml', 'to make pseudoxml-XML files for display purposes'), + ('', 'linkcheck', 'to check all external links for integrity'), + ( + '', + 'doctest', + 'to run all doctests embedded in the documentation ' '(if enabled)', + ), + ('', 'coverage', 'to run coverage check of the documentation (if enabled)'), + ('', 'clean', 'to remove everything in the build directory'), ] @@ -72,15 +75,15 @@ def build_clean(self) -> int: if not path.exists(self.build_dir): return 0 elif not path.isdir(self.build_dir): - print("Error: %r is not a directory!" % self.build_dir) + print('Error: %r is not a directory!' % self.build_dir) return 1 elif source_dir == build_dir: - print("Error: %r is same as source directory!" % self.build_dir) + print('Error: %r is same as source directory!' % self.build_dir) return 1 elif path.commonpath([source_dir, build_dir]) == build_dir: - print("Error: %r directory contains source directory!" % self.build_dir) + print('Error: %r directory contains source directory!' % self.build_dir) return 1 - print("Removing everything under %r..." % self.build_dir) + print('Removing everything under %r...' % self.build_dir) for item in os.listdir(self.build_dir): rmtree(self.build_dir_join(item)) return 0 @@ -89,7 +92,7 @@ def build_help(self) -> None: if not color_terminal(): nocolor() - print(bold("Sphinx v%s" % sphinx.__display_version__)) + print(bold('Sphinx v%s' % sphinx.__display_version__)) print("Please use `make %s' where %s is one of" % ((blue('target'),) * 2)) for osname, bname, description in BUILDERS: if not osname or os.name == osname: @@ -108,29 +111,34 @@ def build_latexpdf(self) -> int: with chdir(self.build_dir_join('latex')): if '-Q' in self.opts: with open('__LATEXSTDOUT__', 'w') as outfile: - returncode = subprocess.call([makecmd, - 'all-pdf', - 'LATEXOPTS=-halt-on-error', - ], - stdout=outfile, - stderr=subprocess.STDOUT, - ) + returncode = subprocess.call( + [ + makecmd, + 'all-pdf', + 'LATEXOPTS=-halt-on-error', + ], + stdout=outfile, + stderr=subprocess.STDOUT, + ) if returncode: - print('Latex error: check %s' % - self.build_dir_join('latex', '__LATEXSTDOUT__') - ) + print( + 'Latex error: check %s' + % self.build_dir_join('latex', '__LATEXSTDOUT__') + ) elif '-q' in self.opts: returncode = subprocess.call( - [makecmd, - 'all-pdf', - 'LATEXOPTS=-halt-on-error', - 'LATEXMKOPTS=-silent', - ], + [ + makecmd, + 'all-pdf', + 'LATEXOPTS=-halt-on-error', + 'LATEXMKOPTS=-silent', + ], ) if returncode: - print('Latex error: check .log file in %s' % - self.build_dir_join('latex') - ) + print( + 'Latex error: check .log file in %s' + % self.build_dir_join('latex') + ) else: returncode = subprocess.call([makecmd, 'all-pdf']) return returncode @@ -184,8 +192,10 @@ def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: doctreedir = self.build_dir_join('doctrees') args = [ - '--builder', builder, - '--doctree-dir', doctreedir, + '--builder', + builder, + '--doctree-dir', + doctreedir, self.source_dir, self.build_dir_join(builder), ] @@ -194,8 +204,11 @@ def run_generic_build(self, builder: str, doctreedir: str | None = None) -> int: def run_make_mode(args: Sequence[str]) -> int: if len(args) < 3: - print('Error: at least 3 arguments (builder, source ' - 'dir, build dir) are required.', file=sys.stderr) + print( + 'Error: at least 3 arguments (builder, source ' + 'dir, build dir) are required.', + file=sys.stderr, + ) return 1 builder_name = args[0] diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 4ae4556ca68..cbc9aafa673 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -13,14 +13,15 @@ # try to import readline, unix specific enhancement try: import readline - if TYPE_CHECKING and sys.platform == "win32": # always false, for type checking + + if TYPE_CHECKING and sys.platform == 'win32': # always false, for type checking raise ImportError READLINE_AVAILABLE = True if readline.__doc__ and 'libedit' in readline.__doc__: - readline.parse_and_bind("bind ^I rl_complete") + readline.parse_and_bind('bind ^I rl_complete') USE_LIBEDIT = True else: - readline.parse_and_bind("tab: complete") + readline.parse_and_bind('tab: complete') USE_LIBEDIT = False except ImportError: READLINE_AVAILABLE = False @@ -90,7 +91,7 @@ class ValidationError(Exception): def is_path(x: str) -> str: x = path.expanduser(x) if not path.isdir(x): - raise ValidationError(__("Please enter a valid path name.")) + raise ValidationError(__('Please enter a valid path name.')) return x @@ -106,7 +107,7 @@ def allow_empty(x: str) -> str: def nonempty(x: str) -> str: if not x: - raise ValidationError(__("Please enter some text.")) + raise ValidationError(__('Please enter some text.')) return x @@ -115,6 +116,7 @@ def val(x: str) -> str: if x not in l: raise ValidationError(__('Please enter one of %s.') % ', '.join(l)) return x + return val @@ -135,7 +137,9 @@ def ok(x: str) -> str: def do_prompt( - text: str, default: str | None = None, validator: Callable[[str], Any] = nonempty, + text: str, + default: str | None = None, + validator: Callable[[str], Any] = nonempty, ) -> str | bool: while True: if default is not None: @@ -205,10 +209,16 @@ def ask_user(d: dict[str, Any]) -> None: * makefile: make Makefile * batchfile: make command file """ - print(bold(__('Welcome to the Sphinx %s quickstart utility.')) % __display_version__) + print( + bold(__('Welcome to the Sphinx %s quickstart utility.')) % __display_version__ + ) print() - print(__('Please enter values for the following settings (just press Enter to\n' - 'accept a default value, if one is given in brackets).')) + print( + __( + 'Please enter values for the following settings (just press Enter to\n' + 'accept a default value, if one is given in brackets).' + ) + ) if 'path' in d: print() @@ -218,90 +228,146 @@ def ask_user(d: dict[str, Any]) -> None: print(__('Enter the root path for documentation.')) d['path'] = do_prompt(__('Root path for the documentation'), '.', is_path) - while path.isfile(path.join(d['path'], 'conf.py')) or \ - path.isfile(path.join(d['path'], 'source', 'conf.py')): + while path.isfile(path.join(d['path'], 'conf.py')) or path.isfile( + path.join(d['path'], 'source', 'conf.py') + ): print() - print(bold(__('Error: an existing conf.py has been found in the ' - 'selected root path.'))) + print( + bold( + __( + 'Error: an existing conf.py has been found in the ' + 'selected root path.' + ) + ) + ) print(__('sphinx-quickstart will not overwrite existing Sphinx projects.')) print() - d['path'] = do_prompt(__('Please enter a new root path (or just Enter to exit)'), - '', is_path_or_empty) + d['path'] = do_prompt( + __('Please enter a new root path (or just Enter to exit)'), + '', + is_path_or_empty, + ) if not d['path']: raise SystemExit(1) if 'sep' not in d: print() - print(__('You have two options for placing the build directory for Sphinx output.\n' - 'Either, you use a directory "_build" within the root path, or you separate\n' - '"source" and "build" directories within the root path.')) - d['sep'] = do_prompt(__('Separate source and build directories (y/n)'), 'n', boolean) + print( + __( + 'You have two options for placing the build directory for Sphinx output.\n' + 'Either, you use a directory "_build" within the root path, or you separate\n' + '"source" and "build" directories within the root path.' + ) + ) + d['sep'] = do_prompt( + __('Separate source and build directories (y/n)'), 'n', boolean + ) if 'dot' not in d: print() - print(__('Inside the root directory, two more directories will be created; "_templates"\n' # NoQA: E501 - 'for custom HTML templates and "_static" for custom stylesheets and other static\n' # NoQA: E501 - 'files. You can enter another prefix (such as ".") to replace the underscore.')) # NoQA: E501 + print( + __( + 'Inside the root directory, two more directories will be created; "_templates"\n' # NoQA: E501 + 'for custom HTML templates and "_static" for custom stylesheets and other static\n' # NoQA: E501 + 'files. You can enter another prefix (such as ".") to replace the underscore.' + ) + ) # NoQA: E501 d['dot'] = do_prompt(__('Name prefix for templates and static dir'), '_', ok) if 'project' not in d: print() - print(__('The project name will occur in several places in the built documentation.')) + print( + __( + 'The project name will occur in several places in the built documentation.' + ) + ) d['project'] = do_prompt(__('Project name')) if 'author' not in d: d['author'] = do_prompt(__('Author name(s)')) if 'version' not in d: print() - print(__('Sphinx has the notion of a "version" and a "release" for the\n' - 'software. Each version can have multiple releases. For example, for\n' - 'Python the version is something like 2.5 or 3.0, while the release is\n' - "something like 2.5.1 or 3.0a1. If you don't need this dual structure,\n" - 'just set both to the same value.')) + print( + __( + 'Sphinx has the notion of a "version" and a "release" for the\n' + 'software. Each version can have multiple releases. For example, for\n' + 'Python the version is something like 2.5 or 3.0, while the release is\n' + "something like 2.5.1 or 3.0a1. If you don't need this dual structure,\n" + 'just set both to the same value.' + ) + ) d['version'] = do_prompt(__('Project version'), '', allow_empty) if 'release' not in d: d['release'] = do_prompt(__('Project release'), d['version'], allow_empty) if 'language' not in d: print() - print(__( - 'If the documents are to be written in a language other than English,\n' - 'you can select a language here by its language code. Sphinx will then\n' - 'translate text that it generates into that language.\n' - '\n' - 'For a list of supported codes, see\n' - 'https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.', - )) + print( + __( + 'If the documents are to be written in a language other than English,\n' + 'you can select a language here by its language code. Sphinx will then\n' + 'translate text that it generates into that language.\n' + '\n' + 'For a list of supported codes, see\n' + 'https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-language.', + ) + ) d['language'] = do_prompt(__('Project language'), 'en') if d['language'] == 'en': d['language'] = None if 'suffix' not in d: print() - print(__('The file name suffix for source files. Commonly, this is either ".txt"\n' - 'or ".rst". Only files with this suffix are considered documents.')) + print( + __( + 'The file name suffix for source files. Commonly, this is either ".txt"\n' + 'or ".rst". Only files with this suffix are considered documents.' + ) + ) d['suffix'] = do_prompt(__('Source file suffix'), '.rst', suffix) if 'master' not in d: print() - print(__('One document is special in that it is considered the top node of the\n' - '"contents tree", that is, it is the root of the hierarchical structure\n' - 'of the documents. Normally, this is "index", but if your "index"\n' - 'document is a custom template, you can also set this to another filename.')) - d['master'] = do_prompt(__('Name of your master document (without suffix)'), 'index') - - while path.isfile(path.join(d['path'], d['master'] + d['suffix'])) or \ - path.isfile(path.join(d['path'], 'source', d['master'] + d['suffix'])): + print( + __( + 'One document is special in that it is considered the top node of the\n' + '"contents tree", that is, it is the root of the hierarchical structure\n' + 'of the documents. Normally, this is "index", but if your "index"\n' + 'document is a custom template, you can also set this to another filename.' + ) + ) + d['master'] = do_prompt( + __('Name of your master document (without suffix)'), 'index' + ) + + while ( + path.isfile(path.join(d['path'], d['master'] + d['suffix'])) + or path.isfile(path.join(d['path'], 'source', d['master'] + d['suffix'])) + ): # fmt: skip print() - print(bold(__('Error: the master file %s has already been found in the ' - 'selected root path.') % (d['master'] + d['suffix']))) + print( + bold( + __( + 'Error: the master file %s has already been found in the ' + 'selected root path.' + ) + % (d['master'] + d['suffix']) + ) + ) print(__('sphinx-quickstart will not overwrite the existing file.')) print() - d['master'] = do_prompt(__('Please enter a new file name, or rename the ' - 'existing file and press Enter'), d['master']) + d['master'] = do_prompt( + __( + 'Please enter a new file name, or rename the ' + 'existing file and press Enter' + ), + d['master'], + ) if 'extensions' not in d: - print(__('Indicate which of the following Sphinx extensions should be enabled:')) + print( + __('Indicate which of the following Sphinx extensions should be enabled:') + ) d['extensions'] = [] for name, description in EXTENSIONS.items(): if do_prompt(f'{name}: {description} (y/n)', 'n', boolean): @@ -309,19 +375,29 @@ def ask_user(d: dict[str, Any]) -> None: # Handle conflicting options if {'sphinx.ext.imgmath', 'sphinx.ext.mathjax'}.issubset(d['extensions']): - print(__('Note: imgmath and mathjax cannot be enabled at the same time. ' - 'imgmath has been deselected.')) + print( + __( + 'Note: imgmath and mathjax cannot be enabled at the same time. ' + 'imgmath has been deselected.' + ) + ) d['extensions'].remove('sphinx.ext.imgmath') if 'makefile' not in d: print() - print(__('A Makefile and a Windows command file can be generated for you so that you\n' - "only have to run e.g. `make html' instead of invoking sphinx-build\n" - 'directly.')) + print( + __( + 'A Makefile and a Windows command file can be generated for you so that you\n' + "only have to run e.g. `make html' instead of invoking sphinx-build\n" + 'directly.' + ) + ) d['makefile'] = do_prompt(__('Create Makefile? (y/n)'), 'y', boolean) if 'batchfile' not in d: - d['batchfile'] = do_prompt(__('Create Windows command file? (y/n)'), 'y', boolean) + d['batchfile'] = do_prompt( + __('Create Windows command file? (y/n)'), 'y', boolean + ) print() @@ -345,7 +421,7 @@ def generate( d.setdefault('extensions', []) d['copyright'] = time.strftime('%Y') + ', ' + d['author'] - d["path"] = os.path.abspath(d['path']) + d['path'] = os.path.abspath(d['path']) ensuredir(d['path']) srcdir = path.join(d['path'], 'source') if d['sep'] else d['path'] @@ -356,10 +432,14 @@ def generate( d['exclude_patterns'] = '' else: builddir = path.join(srcdir, d['dot'] + 'build') - exclude_patterns = map(repr, [ - d['dot'] + 'build', - 'Thumbs.db', '.DS_Store', - ]) + exclude_patterns = map( + repr, + [ + d['dot'] + 'build', + 'Thumbs.db', + '.DS_Store', + ], + ) d['exclude_patterns'] = ', '.join(exclude_patterns) ensuredir(builddir) ensuredir(path.join(srcdir, d['dot'] + 'templates')) @@ -377,17 +457,16 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: conf_path = os.path.join(templatedir, 'conf.py.jinja') if templatedir else None if not conf_path or not path.isfile(conf_path): - conf_path = os.path.join(package_dir, 'templates', 'quickstart', 'conf.py.jinja') - with open(conf_path, encoding="utf-8") as f: + conf_path = os.path.join( + package_dir, 'templates', 'quickstart', 'conf.py.jinja' + ) + with open(conf_path, encoding='utf-8') as f: conf_text = f.read() write_file(path.join(srcdir, 'conf.py'), template.render_string(conf_text, d)) masterfile = path.join(srcdir, d['master'] + d['suffix']) if template._has_custom_template('quickstart/master_doc.rst.jinja'): - msg = ('A custom template `master_doc.rst.jinja` found. It has been renamed to ' - '`root_doc.rst.jinja`. Please rename it on your project too.') - print(colorize('red', msg)) write_file(masterfile, template.render('quickstart/master_doc.rst.jinja', d)) else: write_file(masterfile, template.render('quickstart/root_doc.rst.jinja', d)) @@ -399,30 +478,50 @@ def write_file(fpath: str, content: str, newline: str | None = None) -> None: d['rsrcdir'] = 'source' if d['sep'] else '.' d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' # use binary mode, to avoid writing \r\n on Windows - write_file(path.join(d['path'], 'Makefile'), - template.render(makefile_template, d), '\n') + write_file( + path.join(d['path'], 'Makefile'), + template.render(makefile_template, d), + '\n', + ) if d['batchfile'] is True: d['rsrcdir'] = 'source' if d['sep'] else '.' d['rbuilddir'] = 'build' if d['sep'] else d['dot'] + 'build' - write_file(path.join(d['path'], 'make.bat'), - template.render(batchfile_template, d), '\r\n') + write_file( + path.join(d['path'], 'make.bat'), + template.render(batchfile_template, d), + '\r\n', + ) if silent: return print() print(bold(__('Finished: An initial directory structure has been created.'))) print() - print(__('You should now populate your master file %s and create other documentation\n' - 'source files. ') % masterfile, end='') + print( + __( + 'You should now populate your master file %s and create other documentation\n' + 'source files. ' + ) + % masterfile, + end='', + ) if d['makefile'] or d['batchfile']: - print(__('Use the Makefile to build the docs, like so:\n' - ' make builder')) + print(__('Use the Makefile to build the docs, like so:\n' ' make builder')) else: - print(__('Use the sphinx-build command to build the docs, like so:\n' - ' sphinx-build -b builder %s %s') % (srcdir, builddir)) - print(__('where "builder" is one of the supported builders, ' - 'e.g. html, latex or linkcheck.')) + print( + __( + 'Use the sphinx-build command to build the docs, like so:\n' + ' sphinx-build -b builder %s %s' + ) + % (srcdir, builddir) + ) + print( + __( + 'where "builder" is one of the supported builders, ' + 'e.g. html, latex or linkcheck.' + ) + ) print() @@ -454,83 +553,166 @@ def valid_dir(d: dict[str, Any]) -> bool: def get_parser() -> argparse.ArgumentParser: description = __( - "\n" - "Generate required files for a Sphinx project.\n" - "\n" - "sphinx-quickstart is an interactive tool that asks some questions about your\n" - "project and then generates a complete documentation directory and sample\n" - "Makefile to be used with sphinx-build.\n", + '\n' + 'Generate required files for a Sphinx project.\n' + '\n' + 'sphinx-quickstart is an interactive tool that asks some questions about your\n' + 'project and then generates a complete documentation directory and sample\n' + 'Makefile to be used with sphinx-build.\n', ) parser = argparse.ArgumentParser( usage='%(prog)s [OPTIONS] ', - epilog=__("For more information, visit ."), - description=description) + epilog=__('For more information, visit .'), + description=description, + ) - parser.add_argument('-q', '--quiet', action='store_true', dest='quiet', - default=None, - help=__('quiet mode')) - parser.add_argument('--version', action='version', dest='show_version', - version='%%(prog)s %s' % __display_version__) + parser.add_argument( + '-q', + '--quiet', + action='store_true', + dest='quiet', + default=None, + help=__('quiet mode'), + ) + parser.add_argument( + '--version', + action='version', + dest='show_version', + version='%%(prog)s %s' % __display_version__, + ) - parser.add_argument('path', metavar='PROJECT_DIR', default='.', nargs='?', - help=__('project root')) + parser.add_argument( + 'path', metavar='PROJECT_DIR', default='.', nargs='?', help=__('project root') + ) group = parser.add_argument_group(__('Structure options')) - group.add_argument('--sep', action='store_true', dest='sep', default=None, - help=__('if specified, separate source and build dirs')) - group.add_argument('--no-sep', action='store_false', dest='sep', - help=__('if specified, create build dir under source dir')) - group.add_argument('--dot', metavar='DOT', default='_', - help=__('replacement for dot in _templates etc.')) + group.add_argument( + '--sep', + action='store_true', + dest='sep', + default=None, + help=__('if specified, separate source and build dirs'), + ) + group.add_argument( + '--no-sep', + action='store_false', + dest='sep', + help=__('if specified, create build dir under source dir'), + ) + group.add_argument( + '--dot', + metavar='DOT', + default='_', + help=__('replacement for dot in _templates etc.'), + ) group = parser.add_argument_group(__('Project basic options')) - group.add_argument('-p', '--project', metavar='PROJECT', dest='project', - help=__('project name')) - group.add_argument('-a', '--author', metavar='AUTHOR', dest='author', - help=__('author names')) - group.add_argument('-v', metavar='VERSION', dest='version', default='', - help=__('version of project')) - group.add_argument('-r', '--release', metavar='RELEASE', dest='release', - help=__('release of project')) - group.add_argument('-l', '--language', metavar='LANGUAGE', dest='language', - help=__('document language')) - group.add_argument('--suffix', metavar='SUFFIX', default='.rst', - help=__('source file suffix')) - group.add_argument('--master', metavar='MASTER', default='index', - help=__('master document name')) - group.add_argument('--epub', action='store_true', default=False, - help=__('use epub')) + group.add_argument( + '-p', '--project', metavar='PROJECT', dest='project', help=__('project name') + ) + group.add_argument( + '-a', '--author', metavar='AUTHOR', dest='author', help=__('author names') + ) + group.add_argument( + '-v', + metavar='VERSION', + dest='version', + default='', + help=__('version of project'), + ) + group.add_argument( + '-r', + '--release', + metavar='RELEASE', + dest='release', + help=__('release of project'), + ) + group.add_argument( + '-l', + '--language', + metavar='LANGUAGE', + dest='language', + help=__('document language'), + ) + group.add_argument( + '--suffix', metavar='SUFFIX', default='.rst', help=__('source file suffix') + ) + group.add_argument( + '--master', metavar='MASTER', default='index', help=__('master document name') + ) + group.add_argument( + '--epub', action='store_true', default=False, help=__('use epub') + ) group = parser.add_argument_group(__('Extension options')) for ext in EXTENSIONS: - group.add_argument('--ext-%s' % ext, action='append_const', - const='sphinx.ext.%s' % ext, dest='extensions', - help=__('enable %s extension') % ext) - group.add_argument('--extensions', metavar='EXTENSIONS', dest='extensions', - action='append', help=__('enable arbitrary extensions')) + group.add_argument( + '--ext-%s' % ext, + action='append_const', + const='sphinx.ext.%s' % ext, + dest='extensions', + help=__('enable %s extension') % ext, + ) + group.add_argument( + '--extensions', + metavar='EXTENSIONS', + dest='extensions', + action='append', + help=__('enable arbitrary extensions'), + ) group = parser.add_argument_group(__('Makefile and Batchfile creation')) - group.add_argument('--makefile', action='store_true', dest='makefile', default=True, - help=__('create makefile')) - group.add_argument('--no-makefile', action='store_false', dest='makefile', - help=__('do not create makefile')) - group.add_argument('--batchfile', action='store_true', dest='batchfile', default=True, - help=__('create batchfile')) - group.add_argument('--no-batchfile', action='store_false', - dest='batchfile', - help=__('do not create batchfile')) + group.add_argument( + '--makefile', + action='store_true', + dest='makefile', + default=True, + help=__('create makefile'), + ) + group.add_argument( + '--no-makefile', + action='store_false', + dest='makefile', + help=__('do not create makefile'), + ) + group.add_argument( + '--batchfile', + action='store_true', + dest='batchfile', + default=True, + help=__('create batchfile'), + ) + group.add_argument( + '--no-batchfile', + action='store_false', + dest='batchfile', + help=__('do not create batchfile'), + ) # --use-make-mode is a no-op from Sphinx 8. - group.add_argument('-m', '--use-make-mode', action='store_true', - dest='make_mode', default=True, - help=__('use make-mode for Makefile/make.bat')) + group.add_argument( + '-m', + '--use-make-mode', + action='store_true', + dest='make_mode', + default=True, + help=__('use make-mode for Makefile/make.bat'), + ) group = parser.add_argument_group(__('Project templating')) - group.add_argument('-t', '--templatedir', metavar='TEMPLATEDIR', - dest='templatedir', - help=__('template directory for template files')) - group.add_argument('-d', metavar='NAME=VALUE', action='append', - dest='variables', - help=__('define a template variable')) + group.add_argument( + '-t', + '--templatedir', + metavar='TEMPLATEDIR', + dest='templatedir', + help=__('template directory for template files'), + ) + group.add_argument( + '-d', + metavar='NAME=VALUE', + action='append', + dest='variables', + help=__('define a template variable'), + ) return parser @@ -563,8 +745,12 @@ def main(argv: Sequence[str] = (), /) -> int: try: if 'quiet' in d: if not {'project', 'author'}.issubset(d): - print(__('"quiet" is specified, but any of "project" or ' - '"author" is not specified.')) + print( + __( + '"quiet" is specified, but any of "project" or ' + '"author" is not specified.' + ) + ) return 1 if {'quiet', 'project', 'author'}.issubset(d): @@ -577,10 +763,20 @@ def main(argv: Sequence[str] = (), /) -> int: if not valid_dir(d): print() - print(bold(__('Error: specified path is not a directory, or sphinx' - ' files already exist.'))) - print(__('sphinx-quickstart only generate into a empty directory.' - ' Please specify a new root path.')) + print( + bold( + __( + 'Error: specified path is not a directory, or sphinx' + ' files already exist.' + ) + ) + ) + print( + __( + 'sphinx-quickstart only generate into a empty directory.' + ' Please specify a new root path.' + ) + ) return 1 else: ask_user(d) diff --git a/sphinx/config.py b/sphinx/config.py index 0fd2102e732..134a6e39467 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -619,28 +619,55 @@ def init_numfig_format(app: Sphinx, config: Config) -> None: config.numfig_format = numfig_format +def evaluate_copyright_placeholders(_app: Sphinx, config: Config) -> None: + """Replace copyright year placeholders (%Y) with the current year.""" + replace_yr = str(time.localtime().tm_year) + for k in ('copyright', 'epub_copyright'): + if k in config: + value: str | Sequence[str] = config[k] + if isinstance(value, str): + if '%Y' in value: + config[k] = value.replace('%Y', replace_yr) + else: + if any('%Y' in line for line in value): + items = (line.replace('%Y', replace_yr) for line in value) + config[k] = type(value)(items) # type: ignore[call-arg] + + def correct_copyright_year(_app: Sphinx, config: Config) -> None: """Correct values of copyright year that are not coherent with the SOURCE_DATE_EPOCH environment variable (if set) See https://reproducible-builds.org/specs/source-date-epoch/ """ - if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is None: + if source_date_epoch := int(getenv('SOURCE_DATE_EPOCH', '0')): + source_date_epoch_year = time.gmtime(source_date_epoch).tm_year + else: return - source_date_epoch_year = str(time.gmtime(int(source_date_epoch)).tm_year) + # If the current year is the replacement year, there's no work to do. + # We also skip replacement years that are in the future. + current_year = time.localtime().tm_year + if current_year <= source_date_epoch_year: + return + current_yr = str(current_year) + replace_yr = str(source_date_epoch_year) for k in ('copyright', 'epub_copyright'): if k in config: value: str | Sequence[str] = config[k] if isinstance(value, str): - config[k] = _substitute_copyright_year(value, source_date_epoch_year) + config[k] = _substitute_copyright_year(value, current_yr, replace_yr) else: - items = (_substitute_copyright_year(x, source_date_epoch_year) for x in value) + items = ( + _substitute_copyright_year(x, current_yr, replace_yr) for x in value + ) config[k] = type(value)(items) # type: ignore[call-arg] -def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: +def _substitute_copyright_year( + copyright_line: str, current_year: str, replace_year: str +) -> str: """Replace the year in a single copyright line. Legal formats are: @@ -648,6 +675,7 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: * ``YYYY`` * ``YYYY,`` * ``YYYY `` + * ``YYYY-YYYY`` * ``YYYY-YYYY,`` * ``YYYY-YYYY `` @@ -656,13 +684,17 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: if len(copyright_line) < 4 or not copyright_line[:4].isdigit(): return copyright_line - if copyright_line[4:5] in {'', ' ', ','}: + if copyright_line[:4] == current_year and copyright_line[4:5] in {'', ' ', ','}: return replace_year + copyright_line[4:] - if copyright_line[4] != '-': + if copyright_line[4:5] != '-': return copyright_line - if copyright_line[5:9].isdigit() and copyright_line[9:10] in {'', ' ', ','}: + if ( + copyright_line[5:9].isdigit() + and copyright_line[5:9] == current_year + and copyright_line[9:10] in {'', ' ', ','} + ): return copyright_line[:5] + replace_year + copyright_line[9:] return copyright_line @@ -758,6 +790,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.connect('config-inited', convert_source_suffix, priority=800) app.connect('config-inited', convert_highlight_options, priority=800) app.connect('config-inited', init_numfig_format, priority=800) + app.connect('config-inited', evaluate_copyright_placeholders, priority=795) app.connect('config-inited', correct_copyright_year, priority=800) app.connect('config-inited', check_confval_types, priority=800) app.connect('config-inited', check_primary_domain, priority=800) diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index fb42e5ee314..218bc6d5431 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -111,7 +111,9 @@ def handle_signature(self, sig: str, signode: desc_signature) -> ObjDescT: """ raise ValueError - def add_target_and_index(self, name: ObjDescT, sig: str, signode: desc_signature) -> None: + def add_target_and_index( + self, name: ObjDescT, sig: str, signode: desc_signature + ) -> None: """ Add cross-reference IDs and entries to self.indexnode, if applicable. @@ -228,19 +230,18 @@ def run(self) -> list[Node]: self.options['no-index'] = self.options['noindex'] if 'no-index-entry' not in self.options and 'noindexentry' in self.options: self.options['no-index-entry'] = self.options['noindexentry'] - if 'no-contents-entry' not in self.options and 'nocontentsentry' in self.options: + if ( + 'no-contents-entry' not in self.options + and 'nocontentsentry' in self.options + ): self.options['no-contents-entry'] = self.options['nocontentsentry'] - node['no-index'] = node['noindex'] = no_index = ( - 'no-index' in self.options - ) - node['no-index-entry'] = node['noindexentry'] = ( - 'no-index-entry' in self.options - ) + node['no-index'] = node['noindex'] = no_index = 'no-index' in self.options + node['no-index-entry'] = node['noindexentry'] = 'no-index-entry' in self.options node['no-contents-entry'] = node['nocontentsentry'] = ( 'no-contents-entry' in self.options ) - node['no-typesetting'] = ('no-typesetting' in self.options) + node['no-typesetting'] = 'no-typesetting' in self.options if self.domain: node['classes'].append(self.domain) node['classes'].append(node['objtype']) @@ -287,8 +288,9 @@ def run(self) -> list[Node]: content_node = addnodes.desc_content('', *content_children) node.append(content_node) self.transform_content(content_node) - self.env.app.emit('object-description-transform', - self.domain, self.objtype, content_node) + self.env.app.emit( + 'object-description-transform', self.domain, self.objtype, content_node + ) DocFieldTransformer(self).transform_all(content_node) self.env.temp_data['object'] = None self.after_content() @@ -299,8 +301,11 @@ def run(self) -> list[Node]: # If ``:no-index:`` is set, or there are no ids on the node # or any of its children, then just return the index node, # as Docutils expects a target node to have at least one id. - if node_ids := [node_id for el in node.findall(nodes.Element) # type: ignore[var-annotated] - for node_id in el.get('ids', ())]: + if node_ids := [ # type: ignore[var-annotated] + node_id + for el in node.findall(nodes.Element) + for node_id in el.get('ids', ()) + ]: target_node = nodes.target(ids=node_ids) self.set_source_info(target_node) return [self.indexnode, target_node] @@ -321,16 +326,20 @@ def run(self) -> list[Node]: docutils.unregister_role('') return [] role_name = self.arguments[0] - role, messages = roles.role(role_name, self.state_machine.language, - self.lineno, self.state.reporter) + role, messages = roles.role( + role_name, self.state_machine.language, self.lineno, self.state.reporter + ) if role: docutils.register_role('', role) # type: ignore[arg-type] self.env.temp_data['default_role'] = role_name else: literal_block = nodes.literal_block(self.block_text, self.block_text) reporter = self.state.reporter - error = reporter.error('Unknown interpreted text role "%s".' % role_name, - literal_block, line=self.lineno) + error = reporter.error( + 'Unknown interpreted text role "%s".' % role_name, + literal_block, + line=self.lineno, + ) messages += [error] return cast(list[nodes.Node], messages) @@ -360,7 +369,7 @@ def run(self) -> list[Node]: def setup(app: Sphinx) -> ExtensionMetadata: - app.add_config_value("strip_signature_backslash", False, 'env') + app.add_config_value('strip_signature_backslash', False, 'env') directives.register_directive('default-role', DefaultRole) directives.register_directive('default-domain', DefaultDomain) directives.register_directive('describe', ObjectDescription) diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 5dc42e5b744..bb2e85316a4 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -45,13 +45,15 @@ def run(self) -> list[Node]: force = 'force' in self.options self.env.temp_data['highlight_language'] = language - return [addnodes.highlightlang(lang=language, - force=force, - linenothreshold=linenothreshold)] + return [ + addnodes.highlightlang( + lang=language, force=force, linenothreshold=linenothreshold + ) + ] def dedent_lines( - lines: list[str], dedent: int | None, location: tuple[str, int] | None = None, + lines: list[str], dedent: int | None, location: tuple[str, int] | None = None ) -> list[str]: if dedent is None: return textwrap.dedent(''.join(lines)).splitlines(True) @@ -70,10 +72,11 @@ def dedent_lines( def container_wrapper( - directive: SphinxDirective, literal_node: Node, caption: str, + directive: SphinxDirective, literal_node: Node, caption: str ) -> nodes.container: - container_node = nodes.container('', literal_block=True, - classes=['literal-block-wrapper']) + container_node = nodes.container( + '', literal_block=True, classes=['literal-block-wrapper'] + ) parsed = directive.parse_text_to_nodes(caption, offset=directive.content_offset) node = parsed[0] if isinstance(node, nodes.system_message): @@ -121,9 +124,12 @@ def run(self) -> list[Node]: nlines = len(self.content) hl_lines = parselinenos(linespec, nlines) if any(i >= nlines for i in hl_lines): - logger.warning(__('line number spec is out of range(1-%d): %r'), - nlines, self.options['emphasize-lines'], - location=location) + logger.warning( + __('line number spec is out of range(1-%d): %r'), + nlines, + self.options['emphasize-lines'], + location=location, + ) hl_lines = [x + 1 for x in hl_lines if x < nlines] except ValueError as err: @@ -149,8 +155,9 @@ def run(self) -> list[Node]: # no highlight language specified. Then this directive refers the current # highlight setting via ``highlight`` directive or ``highlight_language`` # configuration. - literal['language'] = self.env.temp_data.get('highlight_language', - self.config.highlight_language) + literal['language'] = self.env.temp_data.get( + 'highlight_language', self.config.highlight_language + ) extra_args = literal['highlight_args'] = {} if hl_lines is not None: extra_args['hl_lines'] = hl_lines @@ -200,11 +207,11 @@ def __init__(self, filename: str, options: dict[str, Any], config: Config) -> No def parse_options(self) -> None: for option1, option2 in self.INVALID_OPTIONS_PAIR: if option1 in self.options and option2 in self.options: - raise ValueError(__('Cannot use both "%s" and "%s" options') % - (option1, option2)) + msg = __('Cannot use both "%s" and "%s" options') % (option1, option2) + raise ValueError(msg) def read_file( - self, filename: str, location: tuple[str, int] | None = None, + self, filename: str, location: tuple[str, int] | None = None ) -> list[str]: try: with open(filename, encoding=self.encoding, errors='strict') as f: @@ -214,24 +221,28 @@ def read_file( return text.splitlines(True) except OSError as exc: - raise OSError(__('Include file %r not found or reading it failed') % - filename) from exc + msg = __('Include file %r not found or reading it failed') % filename + raise OSError(msg) from exc except UnicodeError as exc: - raise UnicodeError(__('Encoding %r used for reading included file %r seems to ' - 'be wrong, try giving an :encoding: option') % - (self.encoding, filename)) from exc + msg = __( + 'Encoding %r used for reading included file %r seems to ' + 'be wrong, try giving an :encoding: option' + ) % (self.encoding, filename) + raise UnicodeError(msg) from exc def read(self, location: tuple[str, int] | None = None) -> tuple[str, int]: if 'diff' in self.options: lines = self.show_diff() else: - filters = [self.pyobject_filter, - self.start_filter, - self.end_filter, - self.lines_filter, - self.dedent_filter, - self.prepend_filter, - self.append_filter] + filters = [ + self.pyobject_filter, + self.start_filter, + self.end_filter, + self.lines_filter, + self.dedent_filter, + self.prepend_filter, + self.append_filter, + ] lines = self.read_file(self.filename, location=location) for func in filters: lines = func(lines, location=location) @@ -246,33 +257,41 @@ def show_diff(self, location: tuple[str, int] | None = None) -> list[str]: return list(diff) def pyobject_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: pyobject = self.options.get('pyobject') if pyobject: from sphinx.pycode import ModuleAnalyzer + analyzer = ModuleAnalyzer.for_file(self.filename, '') tags = analyzer.find_tags() if pyobject not in tags: - raise ValueError(__('Object named %r not found in include file %r') % - (pyobject, self.filename)) + msg = __('Object named %r not found in include file %r') % ( + pyobject, + self.filename, + ) + raise ValueError(msg) start = tags[pyobject][1] end = tags[pyobject][2] - lines = lines[start - 1:end] + lines = lines[start - 1 : end] if 'lineno-match' in self.options: self.lineno_start = start return lines def lines_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: linespec = self.options.get('lines') if linespec: linelist = parselinenos(linespec, len(lines)) if any(i >= len(lines) for i in linelist): - logger.warning(__('line number spec is out of range(1-%d): %r'), - len(lines), linespec, location=location) + logger.warning( + __('line number spec is out of range(1-%d): %r'), + len(lines), + linespec, + location=location, + ) if 'lineno-match' in self.options: # make sure the line list is not "disjoint". @@ -280,18 +299,21 @@ def lines_filter( if all(first + i == n for i, n in enumerate(linelist)): self.lineno_start += linelist[0] else: - raise ValueError(__('Cannot use "lineno-match" with a disjoint ' - 'set of "lines"')) + msg = __('Cannot use "lineno-match" with a disjoint set of "lines"') + raise ValueError(msg) lines = [lines[n] for n in linelist if n < len(lines)] if not lines: - raise ValueError(__('Line spec %r: no lines pulled from include file %r') % - (linespec, self.filename)) + msg = __('Line spec %r: no lines pulled from include file %r') % ( + linespec, + self.filename, + ) + raise ValueError(msg) return lines def start_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: if 'start-at' in self.options: start = self.options.get('start-at') @@ -309,7 +331,7 @@ def start_filter( if 'lineno-match' in self.options: self.lineno_start += lineno + 1 - return lines[lineno + 1:] + return lines[lineno + 1 :] else: if 'lineno-match' in self.options: self.lineno_start += lineno @@ -324,7 +346,7 @@ def start_filter( return lines def end_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: if 'end-at' in self.options: end = self.options.get('end-at') @@ -339,7 +361,7 @@ def end_filter( for lineno, line in enumerate(lines): if end in line: if inclusive: - return lines[:lineno + 1] + return lines[: lineno + 1] else: if lineno == 0: pass # end-before ignores first line @@ -353,7 +375,7 @@ def end_filter( return lines def prepend_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: prepend = self.options.get('prepend') if prepend: @@ -362,7 +384,7 @@ def prepend_filter( return lines def append_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: append = self.options.get('append') if append: @@ -371,7 +393,7 @@ def append_filter( return lines def dedent_filter( - self, lines: list[str], location: tuple[str, int] | None = None, + self, lines: list[str], location: tuple[str, int] | None = None ) -> list[str]: if 'dedent' in self.options: return dedent_lines(lines, self.options.get('dedent'), location=location) @@ -417,8 +439,9 @@ class LiteralInclude(SphinxDirective): def run(self) -> list[Node]: document = self.state.document if not document.settings.file_insertion_enabled: - return [document.reporter.warning('File insertion disabled', - line=self.lineno)] + return [ + document.reporter.warning('File insertion disabled', line=self.lineno) + ] # convert options['diff'] to absolute path if 'diff' in self.options: _, path = self.env.relfn2path(self.options['diff']) @@ -439,17 +462,23 @@ def run(self) -> list[Node]: retnode['language'] = 'udiff' elif 'language' in self.options: retnode['language'] = self.options['language'] - if ('linenos' in self.options or 'lineno-start' in self.options or - 'lineno-match' in self.options): + if ( + 'linenos' in self.options + or 'lineno-start' in self.options + or 'lineno-match' in self.options + ): retnode['linenos'] = True retnode['classes'] += self.options.get('class', []) extra_args = retnode['highlight_args'] = {} if 'emphasize-lines' in self.options: hl_lines = parselinenos(self.options['emphasize-lines'], lines) if any(i >= lines for i in hl_lines): - logger.warning(__('line number spec is out of range(1-%d): %r'), - lines, self.options['emphasize-lines'], - location=location) + logger.warning( + __('line number spec is out of range(1-%d): %r'), + lines, + self.options['emphasize-lines'], + location=location, + ) extra_args['hl_lines'] = [x + 1 for x in hl_lines if x < lines] extra_args['linenostart'] = reader.lineno_start diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 18fdff194ee..e297ceb4b89 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -112,14 +112,17 @@ def parse_content(self, toctree: addnodes.toctree) -> None: if glob and glob_re.match(entry) and not explicit and not url_match: pat_name = docname_join(current_docname, entry) doc_names = sorted( - docname for docname in patfilter(all_docnames, pat_name) + docname + for docname in patfilter(all_docnames, pat_name) # don't include generated documents in globs if docname not in generated_docnames ) if not doc_names: logger.warning( __("toctree glob pattern %r didn't match any documents"), - entry, location=toctree) + entry, + location=toctree, + ) for docname in doc_names: all_docnames.remove(docname) # don't include it again @@ -149,22 +152,26 @@ def parse_content(self, toctree: addnodes.toctree) -> None: if docname not in frozen_all_docnames: if excluded(str(self.env.doc2path(docname, False))): - message = __('toctree contains reference to excluded document %r') + msg = __('toctree contains reference to excluded document %r') subtype = 'excluded' else: - message = __('toctree contains reference to nonexisting document %r') + msg = __('toctree contains reference to nonexisting document %r') subtype = 'not_readable' - logger.warning(message, docname, type='toc', subtype=subtype, - location=toctree) + logger.warning( + msg, docname, type='toc', subtype=subtype, location=toctree + ) self.env.note_reread() continue if docname in all_docnames: all_docnames.remove(docname) else: - logger.warning(__('duplicated entry found in toctree: %s'), docname, - location=toctree) + logger.warning( + __('duplicated entry found in toctree: %s'), + docname, + location=toctree, + ) toctree['entries'].append((title, docname)) toctree['includefiles'].append(docname) @@ -210,7 +217,7 @@ def run(self) -> list[Node]: return ret -class SeeAlso(BaseAdmonition): # type: ignore[misc] +class SeeAlso(BaseAdmonition): """ An admonition mentioning things to look at as reference. """ @@ -273,8 +280,10 @@ class Acks(SphinxDirective): def run(self) -> list[Node]: children = self.parse_content_to_nodes() if len(children) != 1 or not isinstance(children[0], nodes.bullet_list): - logger.warning(__('.. acks content is not a list'), - location=(self.env.docname, self.lineno)) + logger.warning( + __('.. acks content is not a list'), + location=(self.env.docname, self.lineno), + ) return [] return [addnodes.acks('', *children)] @@ -296,8 +305,10 @@ def run(self) -> list[Node]: ncolumns = self.options.get('columns', 2) children = self.parse_content_to_nodes() if len(children) != 1 or not isinstance(children[0], nodes.bullet_list): - logger.warning(__('.. hlist content is not a list'), - location=(self.env.docname, self.lineno)) + logger.warning( + __('.. hlist content is not a list'), + location=(self.env.docname, self.lineno), + ) return [] fulllist = children[0] # create a hlist node where the items are distributed @@ -339,13 +350,16 @@ def run(self) -> list[Node]: memo.title_styles = [] memo.section_level = 0 try: - self.state.nested_parse(self.content, self.content_offset, - node, match_titles=True) + self.state.nested_parse( + self.content, self.content_offset, node, match_titles=True + ) title_styles = memo.title_styles - if (not surrounding_title_styles or - not title_styles or - title_styles[0] not in surrounding_title_styles or - not self.state.parent): + if ( + not surrounding_title_styles + or not title_styles + or title_styles[0] not in surrounding_title_styles + or not self.state.parent + ): # No nested sections so no special handling needed. return [node] # Calculate the depths of the current and nested sections. @@ -380,7 +394,6 @@ class Include(BaseInclude, SphinxDirective): """ def run(self) -> Sequence[Node]: - # To properly emit "include-read" events from included RST text, # we must patch the ``StateMachine.insert_input()`` method. # In the future, docutils will hopefully offer a way for Sphinx @@ -392,7 +405,7 @@ def _insert_input(include_lines: list[str], source: str) -> None: # In docutils 0.18 and later, there are two lines at the end # that act as markers. # We must preserve them and leave them out of the include-read event: - text = "\n".join(include_lines[:-2]) + text = '\n'.join(include_lines[:-2]) path = Path(relpath(abspath(source), start=self.env.srcdir)) docname = self.env.docname @@ -415,8 +428,7 @@ def _insert_input(include_lines: list[str], source: str) -> None: # See https://github.com/python/mypy/issues/2427 for details on the mypy issue self.state_machine.insert_input = _insert_input - if self.arguments[0].startswith('<') and \ - self.arguments[0].endswith('>'): + if self.arguments[0].startswith('<') and self.arguments[0].endswith('>'): # docutils "standard" includes, do not do path processing return super().run() rel_filename, filename = self.env.relfn2path(self.arguments[0]) diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 0dd6dc82a30..3503f2e3411 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -62,10 +62,14 @@ def run(self) -> list[Node]: env = self.state.document.settings.env filename = self.options['file'] if path.exists(filename): - logger.warning(__('":file:" option for csv-table directive now recognizes ' - 'an absolute path as a relative path from source directory. ' - 'Please update your document.'), - location=(env.docname, self.lineno)) + logger.warning( + __( + '":file:" option for csv-table directive now recognizes ' + 'an absolute path as a relative path from source directory. ' + 'Please update your document.' + ), + location=(env.docname, self.lineno), + ) else: abspath = path.join(env.srcdir, os_path(self.options['file'][1:])) docdir = path.dirname(env.doc2path(env.docname)) @@ -94,10 +98,13 @@ def run(self) -> list[Node]: set_classes(self.options) code = '\n'.join(self.content) - node = nodes.literal_block(code, code, - classes=self.options.get('classes', []), - force='force' in self.options, - highlight_args={}) + node = nodes.literal_block( + code, + code, + classes=self.options.get('classes', []), + force='force' in self.options, + highlight_args={}, + ) self.add_name(node) set_source_info(self, node) @@ -108,8 +115,9 @@ def run(self) -> list[Node]: # no highlight language specified. Then this directive refers the current # highlight setting via ``highlight`` directive or ``highlight_language`` # configuration. - node['language'] = self.env.temp_data.get('highlight_language', - self.config.highlight_language) + node['language'] = self.env.temp_data.get( + 'highlight_language', self.config.highlight_language + ) if 'number-lines' in self.options: node['linenos'] = True @@ -138,12 +146,15 @@ def run(self) -> list[Node]: if self.arguments and self.arguments[0]: latex = self.arguments[0] + '\n\n' + latex label = self.options.get('label', self.options.get('name')) - node = nodes.math_block(latex, latex, - classes=self.options.get('class', []), - docname=self.env.docname, - number=None, - label=label, - nowrap='nowrap' in self.options) + node = nodes.math_block( + latex, + latex, + classes=self.options.get('class', []), + docname=self.env.docname, + number=None, + label=label, + nowrap='nowrap' in self.options, + ) self.add_name(node) self.set_source_info(node) @@ -157,7 +168,7 @@ def add_target(self, ret: list[Node]) -> None: # assign label automatically if math_number_all enabled if node['label'] == '' or (self.config.math_number_all and not node['label']): seq = self.env.new_serialno('sphinx.ext.math#equations') - node['label'] = "%s:%d" % (self.env.docname, seq) + node['label'] = f'{self.env.docname}:{seq}' # no targets and numbers are needed if not node['label']: diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 6ae9435c5b1..056769e1ce1 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -262,42 +262,55 @@ def setup(self, app: Sphinx) -> None: # setup domains (must do after all initialization) self.domains._setup() - # initialize config - self._update_config(app.config) + # Initialise config. + # The old config is self.config, restored from the pickled environment. + # The new config is app.config, always recreated from ``conf.py`` + self.config_status, self.config_status_extra = self._config_status( + old_config=self.config, new_config=app.config + ) + self.config = app.config # initialize settings self._update_settings(app.config) - def _update_config(self, config: Config) -> None: - """Update configurations by new one.""" - self.config_status = CONFIG_OK - self.config_status_extra = '' - if self.config is None: - self.config_status = CONFIG_NEW - elif self.config.extensions != config.extensions: - self.config_status = CONFIG_EXTENSIONS_CHANGED - extensions = sorted( - set(self.config.extensions) ^ set(config.extensions)) + @staticmethod + def _config_status( + *, old_config: Config | None, new_config: Config + ) -> tuple[int, str]: + """Report the differences between two Config objects. + + Returns a triple of: + + 1. The new configuration + 2. A status code indicating how the configuration has changed. + 3. A status message indicating what has changed. + """ + if old_config is None: + return CONFIG_NEW, '' + + if old_config.extensions != new_config.extensions: + old_extensions = set(old_config.extensions) + new_extensions = set(new_config.extensions) + extensions = old_extensions ^ new_extensions if len(extensions) == 1: - extension = extensions[0] + extension = extensions.pop() else: - extension = '%d' % (len(extensions),) - self.config_status_extra = f' ({extension!r})' - else: - # check if a config value was changed that affects how - # doctrees are read - for item in config.filter(frozenset({'env'})): - if self.config[item.name] != item.value: - self.config_status = CONFIG_CHANGED - self.config_status_extra = f' ({item.name!r})' - break + extension = f'{len(extensions)}' + return CONFIG_EXTENSIONS_CHANGED, f' ({extension!r})' + + # check if a config value was changed that affects how doctrees are read + for item in new_config.filter(frozenset({'env'})): + if old_config[item.name] != item.value: + return CONFIG_CHANGED, f' ({item.name!r})' - self.config = config + return CONFIG_OK, '' def _update_settings(self, config: Config) -> None: """Update settings by new config.""" self.settings['input_encoding'] = config.source_encoding - self.settings['trim_footnote_reference_space'] = config.trim_footnote_reference_space + self.settings['trim_footnote_reference_space'] = ( + config.trim_footnote_reference_space + ) self.settings['language_code'] = config.language # Allow to disable by 3rd party extension (workaround) @@ -322,9 +335,12 @@ def set_versioning_method( condition = versioning_conditions[method] if self.versioning_condition not in {None, condition}: - raise SphinxError(__('This environment is incompatible with the ' - 'selected builder, please choose another ' - 'doctree directory.')) + msg = __( + 'This environment is incompatible with the ' + 'selected builder, please choose another ' + 'doctree directory.' + ) + raise SphinxError(msg) self.versioning_condition = condition self.versioning_compare = compare @@ -337,8 +353,9 @@ def clear_doc(self, docname: str) -> None: self.domains._clear_doc(docname) - def merge_info_from(self, docnames: Iterable[str], other: BuildEnvironment, - app: Sphinx) -> None: + def merge_info_from( + self, docnames: Iterable[str], other: BuildEnvironment, app: Sphinx + ) -> None: """Merge global information gathered about *docnames* while reading them from the *other* environment. @@ -381,12 +398,13 @@ def relfn2path(self, filename: str, docname: str | None = None) -> tuple[str, st if filename.startswith(('/', os.sep)): rel_fn = filename[1:] else: - docdir = path.dirname(self.doc2path(docname or self.docname, - base=False)) + docdir = path.dirname(self.doc2path(docname or self.docname, base=False)) rel_fn = path.join(docdir, filename) - return (canon_path(path.normpath(rel_fn)), - path.normpath(path.join(self.srcdir, rel_fn))) + return ( + canon_path(path.normpath(rel_fn)), + path.normpath(path.join(self.srcdir, rel_fn)), + ) @property def found_docs(self) -> set[str]: @@ -398,9 +416,11 @@ def find_files(self, config: Config, builder: Builder) -> None: self.found_docs. """ try: - exclude_paths = (self.config.exclude_patterns + - self.config.templates_path + - builder.get_asset_paths()) + exclude_paths = ( + self.config.exclude_patterns + + self.config.templates_path + + builder.get_asset_paths() + ) self.project.discover(exclude_paths, self.config.include_patterns) # Current implementation is applying translated messages in the reading @@ -411,18 +431,25 @@ def find_files(self, config: Config, builder: Builder) -> None: # move i18n process into the writing phase, and remove these lines. if builder.use_message_catalog: # add catalog mo file dependency - repo = CatalogRepository(self.srcdir, self.config.locale_dirs, - self.config.language, self.config.source_encoding) + repo = CatalogRepository( + self.srcdir, + self.config.locale_dirs, + self.config.language, + self.config.source_encoding, + ) mo_paths = {c.domain: c.mo_path for c in repo.catalogs} for docname in self.found_docs: domain = docname_to_domain(docname, self.config.gettext_compact) if domain in mo_paths: self.dependencies[docname].add(mo_paths[domain]) except OSError as exc: - raise DocumentError(__('Failed to scan documents in %s: %r') % - (self.srcdir, exc)) from exc + raise DocumentError( + __('Failed to scan documents in %s: %r') % (self.srcdir, exc) + ) from exc - def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str], set[str]]: + def get_outdated_files( + self, config_changed: bool + ) -> tuple[set[str], set[str], set[str]]: """Return (added, changed, removed) sets.""" # clear all files no longer present removed = set(self.all_docs) - self.found_docs @@ -454,10 +481,12 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str], mtime = self.all_docs[docname] newmtime = _last_modified_time(self.doc2path(docname)) if newmtime > mtime: - logger.debug('[build target] outdated %r: %s -> %s', - docname, - _format_rfc3339_microseconds(mtime), - _format_rfc3339_microseconds(newmtime)) + logger.debug( + '[build target] outdated %r: %s -> %s', + docname, + _format_rfc3339_microseconds(mtime), + _format_rfc3339_microseconds(newmtime), + ) changed.add(docname) continue # finally, check the mtime of dependencies @@ -468,7 +497,8 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str], if not path.isfile(deppath): logger.debug( '[build target] changed %r missing dependency %r', - docname, deppath, + docname, + deppath, ) changed.add(docname) break @@ -476,7 +506,8 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str], if depmtime > mtime: logger.debug( '[build target] outdated %r from dependency %r: %s -> %s', - docname, deppath, + docname, + deppath, _format_rfc3339_microseconds(mtime), _format_rfc3339_microseconds(depmtime), ) @@ -610,7 +641,10 @@ def get_and_resolve_doctree( # now, resolve all toctree nodes for toctreenode in doctree.findall(addnodes.toctree): result = toctree_adapters._resolve_toctree( - self, docname, builder, toctreenode, + self, + docname, + builder, + toctreenode, prune=prune_toctrees, includehidden=includehidden, ) @@ -621,9 +655,17 @@ def get_and_resolve_doctree( return doctree - def resolve_toctree(self, docname: str, builder: Builder, toctree: addnodes.toctree, - prune: bool = True, maxdepth: int = 0, titles_only: bool = False, - collapse: bool = False, includehidden: bool = False) -> Node | None: + def resolve_toctree( + self, + docname: str, + builder: Builder, + toctree: addnodes.toctree, + prune: bool = True, + maxdepth: int = 0, + titles_only: bool = False, + collapse: bool = False, + includehidden: bool = False, + ) -> Node | None: """Resolve a *toctree* node into individual bullet lists with titles as items, returning None (if no containing titles are found) or a new node. @@ -636,7 +678,10 @@ def resolve_toctree(self, docname: str, builder: Builder, toctree: addnodes.toct be collapsed. """ return toctree_adapters._resolve_toctree( - self, docname, builder, toctree, + self, + docname, + builder, + toctree, prune=prune, maxdepth=maxdepth, titles_only=titles_only, @@ -644,8 +689,9 @@ def resolve_toctree(self, docname: str, builder: Builder, toctree: addnodes.toct includehidden=includehidden, ) - def resolve_references(self, doctree: nodes.document, fromdocname: str, - builder: Builder) -> None: + def resolve_references( + self, doctree: nodes.document, fromdocname: str, builder: Builder + ) -> None: self.apply_post_transforms(doctree, fromdocname) def apply_post_transforms(self, doctree: nodes.document, docname: str) -> None: @@ -670,7 +716,7 @@ def collect_relations(self) -> dict[str, list[str | None]]: relations = {} docnames = _traverse_toctree( - traversed, None, self.config.root_doc, self.toctree_includes, + traversed, None, self.config.root_doc, self.toctree_includes ) prev_doc = None parent, docname = next(docnames) @@ -697,8 +743,9 @@ def check_consistency(self) -> None: continue if 'orphan' in self.metadata[docname]: continue - logger.warning(__("document isn't included in any toctree"), - location=docname) + logger.warning( + __("document isn't included in any toctree"), location=docname + ) # call check-consistency for all extensions self.domains._check_consistency() @@ -712,9 +759,12 @@ def _traverse_toctree( toctree_includes: dict[str, list[str]], ) -> Iterator[tuple[str | None, str]]: if parent == docname: - logger.warning(__('self referenced toctree found. Ignored.'), - location=docname, type='toc', - subtype='circular') + logger.warning( + __('self referenced toctree found. Ignored.'), + location=docname, + type='toc', + subtype='circular', + ) return # traverse toctree by pre-order @@ -723,7 +773,7 @@ def _traverse_toctree( for child in toctree_includes.get(docname, ()): for sub_parent, sub_docname in _traverse_toctree( - traversed, docname, child, toctree_includes, + traversed, docname, child, toctree_includes ): if sub_docname not in traversed: yield sub_parent, sub_docname diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index fbceeb56b0c..42ae170b233 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -40,10 +40,10 @@ tuple[ _IndexEntryTargets, list[tuple[str, _IndexEntryTargets]], - _IndexEntryCategoryKey - ] + _IndexEntryCategoryKey, + ], ] - ] + ], ] ] @@ -80,41 +80,77 @@ def create_index( try: entry, sub_entry = _split_into(2, 'single', value) except ValueError: - entry, = _split_into(1, 'single', value) + (entry,) = _split_into(1, 'single', value) sub_entry = '' - _add_entry(entry, sub_entry, main, - dic=new, link=uri, key=category_key) + _add_entry( + entry, sub_entry, main, dic=new, link=uri, key=category_key + ) elif entry_type == 'pair': first, second = _split_into(2, 'pair', value) - _add_entry(first, second, main, - dic=new, link=uri, key=category_key) - _add_entry(second, first, main, - dic=new, link=uri, key=category_key) + _add_entry( + first, second, main, dic=new, link=uri, key=category_key + ) + _add_entry( + second, first, main, dic=new, link=uri, key=category_key + ) elif entry_type == 'triple': first, second, third = _split_into(3, 'triple', value) - _add_entry(first, second + ' ' + third, main, - dic=new, link=uri, key=category_key) - _add_entry(second, third + ', ' + first, main, - dic=new, link=uri, key=category_key) - _add_entry(third, first + ' ' + second, main, - dic=new, link=uri, key=category_key) + _add_entry( + first, + second + ' ' + third, + main, + dic=new, + link=uri, + key=category_key, + ) + _add_entry( + second, + third + ', ' + first, + main, + dic=new, + link=uri, + key=category_key, + ) + _add_entry( + third, + first + ' ' + second, + main, + dic=new, + link=uri, + key=category_key, + ) elif entry_type == 'see': first, second = _split_into(2, 'see', value) - _add_entry(first, _('see %s') % second, None, - dic=new, link=False, key=category_key) + _add_entry( + first, + _('see %s') % second, + None, + dic=new, + link=False, + key=category_key, + ) elif entry_type == 'seealso': first, second = _split_into(2, 'see', value) - _add_entry(first, _('see also %s') % second, None, - dic=new, link=False, key=category_key) + _add_entry( + first, + _('see also %s') % second, + None, + dic=new, + link=False, + key=category_key, + ) else: - logger.warning(__('unknown index entry type %r'), entry_type, - location=docname) + logger.warning( + __('unknown index entry type %r'), + entry_type, + location=docname, + ) except ValueError as err: logger.warning(str(err), location=docname) - for (targets, sub_items, _category_key) in new.values(): + for targets, sub_items, _category_key in new.values(): targets.sort(key=_key_func_0) - for (sub_targets, _sub_category_key) in sub_items.values(): + for sub_targets, _sub_category_key in sub_items.values(): sub_targets.sort(key=_key_func_0) new_list: list[tuple[str, _IndexEntry]] = sorted(new.items(), key=_key_func_1) @@ -139,8 +175,8 @@ def create_index( if old_key == m.group(1): # prefixes match: add entry as subitem of the # previous entry - old_sub_items.setdefault( - m.group(2), ([], category_key))[0].extend(targets) + prev = old_sub_items.setdefault(m[2], ([], category_key)) + prev[0].extend(targets) del new_list[i] continue old_key = m.group(1) @@ -150,14 +186,13 @@ def create_index( i += 1 grouped = [] - for (group_key, group) in groupby(new_list, _group_by_func): + for group_key, group in groupby(new_list, _group_by_func): group_list = [] for group_entry in group: entry_key, (targets, sub_items, category_key) = group_entry pairs = [ (sub_key, sub_targets) - for (sub_key, (sub_targets, _sub_category_key)) - in sub_items.items() + for (sub_key, (sub_targets, _sub_category_key)) in sub_items.items() ] pairs.sort(key=_key_func_2) group_list.append((entry_key, (targets, pairs, category_key))) @@ -165,9 +200,15 @@ def create_index( return grouped -def _add_entry(word: str, subword: str, main: str | None, *, - dic: _IndexEntryMap, - link: str | Literal[False], key: _IndexEntryCategoryKey) -> None: +def _add_entry( + word: str, + subword: str, + main: str | None, + *, + dic: _IndexEntryMap, + link: str | Literal[False], + key: _IndexEntryCategoryKey, +) -> None: entry = dic.setdefault(word, ([], {}, key)) if subword: targets = entry[1].setdefault(subword, ([], key))[0] diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index 217cb0d41ef..edd873b5f44 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -24,7 +24,9 @@ logger = logging.getLogger(__name__) -def note_toctree(env: BuildEnvironment, docname: str, toctreenode: addnodes.toctree) -> None: +def note_toctree( + env: BuildEnvironment, docname: str, toctreenode: addnodes.toctree +) -> None: """Note a TOC tree directive in a document and gather information about file relations from it. """ @@ -87,9 +89,7 @@ def global_toctree_for_doc( ) for toctree_node in env.master_doctree.findall(addnodes.toctree) ) - toctrees = [ - toctree for toctree in resolved if toctree is not None - ] + toctrees = [toctree for toctree in resolved if toctree is not None] if not toctrees: return None @@ -100,9 +100,16 @@ def global_toctree_for_doc( def _resolve_toctree( - env: BuildEnvironment, docname: str, builder: Builder, toctree: addnodes.toctree, *, - prune: bool = True, maxdepth: int = 0, titles_only: bool = False, - collapse: bool = False, includehidden: bool = False, + env: BuildEnvironment, + docname: str, + builder: Builder, + toctree: addnodes.toctree, + *, + prune: bool = True, + maxdepth: int = 0, + titles_only: bool = False, + collapse: bool = False, + includehidden: bool = False, ) -> Element | None: """Resolve a *toctree* node into individual bullet lists with titles as items, returning None (if no containing titles are found) or @@ -178,9 +185,13 @@ def _resolve_toctree( # prune the tree to maxdepth, also set toc depth and current classes _toctree_add_classes(newnode, 1, docname) - newnode = _toctree_copy(newnode, 1, maxdepth if prune else 0, collapse, builder.tags) + newnode = _toctree_copy( + newnode, 1, maxdepth if prune else 0, collapse, builder.tags + ) - if isinstance(newnode[-1], nodes.Element) and len(newnode[-1]) == 0: # No titles found + if ( + isinstance(newnode[-1], nodes.Element) and len(newnode[-1]) == 0 + ): # No titles found return None # set the target paths in the toctrees (they are not known at TOC @@ -208,11 +219,20 @@ def _entries_from_toctree( ) -> list[Element]: """Return TOC entries for a toctree node.""" entries: list[Element] = [] - for (title, ref) in toctreenode['entries']: + for title, ref in toctreenode['entries']: try: toc, refdoc = _toctree_entry( - title, ref, env, prune, collapse, tags, toctree_ancestors, - included, excluded, toctreenode, parents, + title, + ref, + env, + prune, + collapse, + tags, + toctree_ancestors, + included, + excluded, + toctreenode, + parents, ) except LookupError: continue @@ -294,10 +314,14 @@ def _toctree_entry( toc = _toctree_generated_entry(title, ref) else: if ref in parents: - logger.warning(__('circular toctree references ' - 'detected, ignoring: %s <- %s'), - ref, ' <- '.join(parents), - location=ref, type='toc', subtype='circular') + logger.warning( + __('circular toctree references ' 'detected, ignoring: %s <- %s'), + ref, + ' <- '.join(parents), + location=ref, + type='toc', + subtype='circular', + ) msg = 'circular reference' raise LookupError(msg) @@ -314,9 +338,16 @@ def _toctree_entry( if not toc.children: # empty toc means: no titles will show up in the toctree - logger.warning(__('toctree contains reference to document %r that ' - "doesn't have a title: no link will be generated"), - ref, location=toctreenode, type='toc', subtype='no_title') + logger.warning( + __( + 'toctree contains reference to document %r that ' + "doesn't have a title: no link will be generated" + ), + ref, + location=toctreenode, + type='toc', + subtype='no_title', + ) except KeyError: # this is raised if the included file does not exist ref_path = str(env.doc2path(ref, False)) @@ -335,9 +366,9 @@ def _toctree_entry( def _toctree_url_entry(title: str, ref: str) -> nodes.bullet_list: if title is None: title = ref - reference = nodes.reference('', '', internal=False, - refuri=ref, anchorname='', - *[nodes.Text(title)]) + reference = nodes.reference( + '', '', internal=False, refuri=ref, anchorname='', *[nodes.Text(title)] + ) para = addnodes.compact_paragraph('', '', reference) item = nodes.list_item('', para) toc = nodes.bullet_list('', item) @@ -345,16 +376,17 @@ def _toctree_url_entry(title: str, ref: str) -> nodes.bullet_list: def _toctree_self_entry( - title: str, ref: str, titles: dict[str, nodes.title], + title: str, + ref: str, + titles: dict[str, nodes.title], ) -> nodes.bullet_list: # 'self' refers to the document from which this # toctree originates if not title: title = clean_astext(titles[ref]) - reference = nodes.reference('', '', internal=True, - refuri=ref, - anchorname='', - *[nodes.Text(title)]) + reference = nodes.reference( + '', '', internal=True, refuri=ref, anchorname='', *[nodes.Text(title)] + ) para = addnodes.compact_paragraph('', '', reference) item = nodes.list_item('', para) # don't show subitems @@ -368,8 +400,7 @@ def _toctree_generated_entry(title: str, ref: str) -> nodes.bullet_list: docname, sectionname = StandardDomain._virtual_doc_names[ref] if not title: title = sectionname - reference = nodes.reference('', title, internal=True, - refuri=docname, anchorname='') + reference = nodes.reference('', title, internal=True, refuri=docname, anchorname='') para = addnodes.compact_paragraph('', '', reference) item = nodes.list_item('', para) # don't show subitems @@ -434,11 +465,13 @@ def _toctree_add_classes(node: Element, depth: int, docname: str) -> None: ET = TypeVar('ET', bound=Element) -def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tags) -> ET: +def _toctree_copy( + node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tags +) -> ET: """Utility: Cut and deep-copy a TOC at a specified depth.""" - keep_bullet_list_sub_nodes = (depth <= 1 - or ((depth <= maxdepth or maxdepth <= 0) - and (not collapse or 'iscurrent' in node))) + keep_bullet_list_sub_nodes = depth <= 1 or ( + (depth <= maxdepth or maxdepth <= 0) and (not collapse or 'iscurrent' in node) + ) copy = node.copy() for subnode in node.children: @@ -459,9 +492,15 @@ def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tag # only keep children if the only node matches the tags if _only_node_keep_children(subnode, tags): for child in subnode.children: - copy.append(_toctree_copy( - child, depth, maxdepth, collapse, tags, # type: ignore[type-var] - )) + copy.append( + _toctree_copy( + child, + depth, + maxdepth, + collapse, + tags, # type: ignore[type-var] + ) + ) elif isinstance(subnode, nodes.reference | nodes.title): # deep copy references and captions sub_node_copy = subnode.copy() @@ -476,7 +515,8 @@ def _toctree_copy(node: ET, depth: int, maxdepth: int, collapse: bool, tags: Tag def _get_toctree_ancestors( - toctree_includes: dict[str, list[str]], docname: str, + toctree_includes: dict[str, list[str]], + docname: str, ) -> Set[str]: parent: dict[str, str] = {} for p, children in toctree_includes.items(): @@ -497,11 +537,22 @@ def __init__(self, env: BuildEnvironment) -> None: def note(self, docname: str, toctreenode: addnodes.toctree) -> None: note_toctree(self.env, docname, toctreenode) - def resolve(self, docname: str, builder: Builder, toctree: addnodes.toctree, - prune: bool = True, maxdepth: int = 0, titles_only: bool = False, - collapse: bool = False, includehidden: bool = False) -> Element | None: + def resolve( + self, + docname: str, + builder: Builder, + toctree: addnodes.toctree, + prune: bool = True, + maxdepth: int = 0, + titles_only: bool = False, + collapse: bool = False, + includehidden: bool = False, + ) -> Element | None: return _resolve_toctree( - self.env, docname, builder, toctree, + self.env, + docname, + builder, + toctree, prune=prune, maxdepth=maxdepth, titles_only=titles_only, @@ -516,6 +567,12 @@ def get_toc_for(self, docname: str, builder: Builder) -> Node: return document_toc(self.env, docname, self.env.app.builder.tags) def get_toctree_for( - self, docname: str, builder: Builder, collapse: bool, **kwargs: Any, + self, + docname: str, + builder: Builder, + collapse: bool, + **kwargs: Any, ) -> Element | None: - return global_toctree_for_doc(self.env, docname, builder, collapse=collapse, **kwargs) + return global_toctree_for_doc( + self.env, docname, builder, collapse=collapse, **kwargs + ) diff --git a/sphinx/environment/collectors/__init__.py b/sphinx/environment/collectors/__init__.py index 52b5a60b4e2..6bb62fc42c1 100644 --- a/sphinx/environment/collectors/__init__.py +++ b/sphinx/environment/collectors/__init__.py @@ -29,10 +29,10 @@ class EnvironmentCollector: def enable(self, app: Sphinx) -> None: assert self.listener_ids is None self.listener_ids = { - 'doctree-read': app.connect('doctree-read', self.process_doc), - 'env-merge-info': app.connect('env-merge-info', self.merge_other), - 'env-purge-doc': app.connect('env-purge-doc', self.clear_doc), - 'env-get-updated': app.connect('env-get-updated', self.get_updated_docs), + 'doctree-read': app.connect('doctree-read', self.process_doc), + 'env-merge-info': app.connect('env-merge-info', self.merge_other), + 'env-purge-doc': app.connect('env-purge-doc', self.clear_doc), + 'env-get-updated': app.connect('env-get-updated', self.get_updated_docs), 'env-get-outdated': app.connect('env-get-outdated', self.get_outdated_docs), } @@ -51,8 +51,13 @@ def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: """ raise NotImplementedError - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: """Merge in specified data regarding docnames from a different `BuildEnvironment` object which coming from a subprocess in parallel builds. @@ -78,8 +83,14 @@ def get_updated_docs(self, app: Sphinx, env: BuildEnvironment) -> list[str]: """ return [] - def get_outdated_docs(self, app: Sphinx, env: BuildEnvironment, - added: set[str], changed: set[str], removed: set[str]) -> list[str]: + def get_outdated_docs( + self, + app: Sphinx, + env: BuildEnvironment, + added: set[str], + changed: set[str], + removed: set[str], + ) -> list[str]: """Return a list of docnames to re-read. This method is called before reading the documents. diff --git a/sphinx/environment/collectors/asset.py b/sphinx/environment/collectors/asset.py index 368e4773290..5096a9d1a68 100644 --- a/sphinx/environment/collectors/asset.py +++ b/sphinx/environment/collectors/asset.py @@ -33,8 +33,13 @@ class ImageCollector(EnvironmentCollector): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: env.images.purge_doc(docname) - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: env.images.merge_other(docnames, other.images) def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: @@ -86,17 +91,26 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: for imgpath in candidates.values(): app.env.dependencies[docname].add(imgpath) if not os.access(path.join(app.srcdir, imgpath), os.R_OK): - logger.warning(__('image file not readable: %s'), imgpath, - location=node, type='image', subtype='not_readable') + logger.warning( + __('image file not readable: %s'), + imgpath, + location=node, + type='image', + subtype='not_readable', + ) continue app.env.images.add_file(docname, imgpath) - def collect_candidates(self, env: BuildEnvironment, imgpath: str, - candidates: dict[str, str], node: Node) -> None: + def collect_candidates( + self, + env: BuildEnvironment, + imgpath: str, + candidates: dict[str, str], + node: Node, + ) -> None: globbed: dict[str, list[str]] = {} for filename in glob(imgpath): - new_imgpath = relative_path(path.join(env.srcdir, 'dummy'), - filename) + new_imgpath = relative_path(path.join(env.srcdir, 'dummy'), filename) try: mimetype = guess_mimetype(filename) if mimetype is None: @@ -105,8 +119,14 @@ def collect_candidates(self, env: BuildEnvironment, imgpath: str, if mimetype not in candidates: globbed.setdefault(mimetype, []).append(new_imgpath) except OSError as err: - logger.warning(__('image file %s not readable: %s'), filename, err, - location=node, type='image', subtype='not_readable') + logger.warning( + __('image file %s not readable: %s'), + filename, + err, + location=node, + type='image', + subtype='not_readable', + ) for key, files in globbed.items(): candidates[key] = min(files, key=len) # select by similarity @@ -117,8 +137,13 @@ class DownloadFileCollector(EnvironmentCollector): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: env.dlfiles.purge_doc(docname) - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: env.dlfiles.merge_other(docnames, other.dlfiles) def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: @@ -131,10 +156,17 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: rel_filename, filename = app.env.relfn2path(targetname, app.env.docname) app.env.dependencies[app.env.docname].add(rel_filename) if not os.access(filename, os.R_OK): - logger.warning(__('download file not readable: %s'), filename, - location=node, type='download', subtype='not_readable') + logger.warning( + __('download file not readable: %s'), + filename, + location=node, + type='download', + subtype='not_readable', + ) continue - node['filename'] = app.env.dlfiles.add_file(app.env.docname, rel_filename) + node['filename'] = app.env.dlfiles.add_file( + app.env.docname, rel_filename + ) def setup(app: Sphinx) -> ExtensionMetadata: diff --git a/sphinx/environment/collectors/dependencies.py b/sphinx/environment/collectors/dependencies.py index 33b54b824a8..46fbf323609 100644 --- a/sphinx/environment/collectors/dependencies.py +++ b/sphinx/environment/collectors/dependencies.py @@ -25,8 +25,13 @@ class DependenciesCollector(EnvironmentCollector): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: env.dependencies.pop(docname, None) - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: for docname in docnames: if docname in other.dependencies: env.dependencies[docname] = other.dependencies[docname] @@ -43,8 +48,7 @@ def process_doc(self, app: Sphinx, doctree: nodes.document) -> None: # one relative to the srcdir if isinstance(dep, bytes): dep = dep.decode(fs_encoding) - relpath = relative_path(frompath, - path.normpath(path.join(cwd, dep))) + relpath = relative_path(frompath, path.normpath(path.join(cwd, dep))) app.env.dependencies[app.env.docname].add(relpath) diff --git a/sphinx/environment/collectors/metadata.py b/sphinx/environment/collectors/metadata.py index bef35119e3a..aadc6af1613 100644 --- a/sphinx/environment/collectors/metadata.py +++ b/sphinx/environment/collectors/metadata.py @@ -20,8 +20,13 @@ class MetadataCollector(EnvironmentCollector): def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: env.metadata.pop(docname, None) - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: for docname in docnames: env.metadata[docname] = other.metadata[docname] diff --git a/sphinx/environment/collectors/title.py b/sphinx/environment/collectors/title.py index 76e3f0379f5..830c8702cf3 100644 --- a/sphinx/environment/collectors/title.py +++ b/sphinx/environment/collectors/title.py @@ -22,8 +22,13 @@ def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: env.titles.pop(docname, None) env.longtitles.pop(docname, None) - def merge_other(self, app: Sphinx, env: BuildEnvironment, - docnames: set[str], other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: for docname in docnames: env.titles[docname] = other.titles[docname] env.longtitles[docname] = other.longtitles[docname] diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index f004fc6820a..1de7b2473eb 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -43,8 +43,13 @@ def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: if not fnset: del env.files_to_rebuild[subfn] - def merge_other(self, app: Sphinx, env: BuildEnvironment, docnames: set[str], - other: BuildEnvironment) -> None: + def merge_other( + self, + app: Sphinx, + env: BuildEnvironment, + docnames: set[str], + other: BuildEnvironment, + ) -> None: for docname in docnames: env.tocs[docname] = other.tocs[docname] env.toc_num_entries[docname] = other.toc_num_entries[docname] @@ -84,8 +89,13 @@ def build_toc( # make these nodes: # list_item -> compact_paragraph -> reference reference = nodes.reference( - '', '', internal=True, refuri=docname, - anchorname=anchorname, *nodetext) + '', + '', + internal=True, + refuri=docname, + anchorname=anchorname, + *nodetext, + ) para = addnodes.compact_paragraph('', '', reference) item: Element = nodes.list_item('', para) sub_item = build_toc(sectionnode, depth + 1) @@ -136,15 +146,23 @@ def build_toc( anchorname = _make_anchor_name(ids, numentries) reference = nodes.reference( - '', '', nodes.literal('', sig_node['_toc_name']), - internal=True, refuri=docname, anchorname=anchorname) - para = addnodes.compact_paragraph('', '', reference, - skip_section_number=True) + '', + '', + nodes.literal('', sig_node['_toc_name']), + internal=True, + refuri=docname, + anchorname=anchorname, + ) + para = addnodes.compact_paragraph( + '', '', reference, skip_section_number=True + ) entry = nodes.list_item('', para) # Find parent node parent = sig_node.parent - while parent not in memo_parents and parent != sectionnode: + while ( + parent not in memo_parents and parent != sectionnode + ): parent = parent.parent # Note, it may both be the limit and in memo_parents, # prefer memo_parents, so we get the nesting. @@ -231,14 +249,21 @@ def _walk_toc( def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None: if depth == 0: return - for (_title, ref) in toctreenode['entries']: + for _title, ref in toctreenode['entries']: if url_re.match(ref) or ref == 'self': # don't mess with those continue if ref in assigned: - logger.warning(__('%s is already assigned section numbers ' - '(nested numbered toctree?)'), ref, - location=toctreenode, type='toc', subtype='secnum') + logger.warning( + __( + '%s is already assigned section numbers ' + '(nested numbered toctree?)' + ), + ref, + location=toctreenode, + type='toc', + subtype='secnum', + ) elif ref in env.tocs: secnums: dict[str, tuple[int, ...]] = {} env.toc_secnumbers[ref] = secnums @@ -273,7 +298,9 @@ def assign_figure_numbers(self, env: BuildEnvironment) -> list[str]: def get_figtype(node: Node) -> str | None: for domain in env.domains.sorted(): figtype = domain.get_enumerable_node_type(node) - if isinstance(domain, StandardDomain) and not domain.get_numfig_title(node): + if isinstance(domain, StandardDomain) and not domain.get_numfig_title( + node + ): # Skip if uncaptioned node continue @@ -292,22 +319,27 @@ def get_section_number(docname: str, section: nodes.section) -> tuple[int, ...]: return secnum or () - def get_next_fignumber(figtype: str, secnum: tuple[int, ...]) -> tuple[int, ...]: + def get_next_fignumber( + figtype: str, secnum: tuple[int, ...] + ) -> tuple[int, ...]: counter = fignum_counter.setdefault(figtype, {}) - secnum = secnum[:env.config.numfig_secnum_depth] + secnum = secnum[: env.config.numfig_secnum_depth] counter[secnum] = counter.get(secnum, 0) + 1 return (*secnum, counter[secnum]) - def register_fignumber(docname: str, secnum: tuple[int, ...], - figtype: str, fignode: Element) -> None: + def register_fignumber( + docname: str, secnum: tuple[int, ...], figtype: str, fignode: Element + ) -> None: env.toc_fignumbers.setdefault(docname, {}) fignumbers = env.toc_fignumbers[docname].setdefault(figtype, {}) figure_id = fignode['ids'][0] fignumbers[figure_id] = get_next_fignumber(figtype, secnum) - def _walk_doctree(docname: str, doctree: Element, secnum: tuple[int, ...]) -> None: + def _walk_doctree( + docname: str, doctree: Element, secnum: tuple[int, ...] + ) -> None: nonlocal generated_docnames for subnode in doctree.children: if isinstance(subnode, nodes.section): diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4f6e776d656..ed2b750f3c0 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1079,7 +1079,8 @@ def get_object_members(self, want_all: bool) -> tuple[bool, list[ObjectMember]]: else: logger.warning(__('missing attribute mentioned in :members: option: ' 'module %s, attribute %s'), - safe_getattr(self.object, '__name__', '???', name), + safe_getattr(self.object, '__name__', '???'), + name, type='autodoc') return False, ret diff --git a/sphinx/ext/intersphinx/_cli.py b/sphinx/ext/intersphinx/_cli.py index 25ec6ca7c8c..04ac2876291 100644 --- a/sphinx/ext/intersphinx/_cli.py +++ b/sphinx/ext/intersphinx/_cli.py @@ -10,9 +10,11 @@ def inspect_main(argv: list[str], /) -> int: """Debug functionality to print out an inventory""" if len(argv) < 1: - print('Print out an inventory file.\n' - 'Error: must specify local path or URL to an inventory file.', - file=sys.stderr) + print( + 'Print out an inventory file.\n' + 'Error: must specify local path or URL to an inventory file.', + file=sys.stderr, + ) return 1 class MockConfig: @@ -27,7 +29,7 @@ class MockConfig: target_uri='', inv_location=filename, config=MockConfig(), # type: ignore[arg-type] - srcdir='' # type: ignore[arg-type] + srcdir='', # type: ignore[arg-type] ) for key in sorted(inv_data or {}): print(key) diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py index 53324f7103f..b891d0d999a 100644 --- a/sphinx/ext/intersphinx/_load.py +++ b/sphinx/ext/intersphinx/_load.py @@ -89,8 +89,10 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None: # ensure target URIs are non-empty and unique if not uri or not isinstance(uri, str): errors += 1 - msg = __('Invalid target URI value `%r` in intersphinx_mapping[%r][0]. ' - 'Target URIs must be unique non-empty strings.') + msg = __( + 'Invalid target URI value `%r` in intersphinx_mapping[%r][0]. ' + 'Target URIs must be unique non-empty strings.' + ) LOGGER.error(msg, uri, name) del config.intersphinx_mapping[name] continue @@ -105,9 +107,12 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None: continue seen[uri] = name + if not isinstance(inv, tuple | list): + inv = (inv,) + # ensure inventory locations are None or non-empty targets: list[InventoryLocation] = [] - for target in (inv if isinstance(inv, (tuple | list)) else (inv,)): + for target in inv: if target is None or target and isinstance(target, str): targets.append(target) else: @@ -143,9 +148,13 @@ def load_mappings(app: Sphinx) -> None: projects = [] for name, (uri, locations) in intersphinx_mapping.values(): try: - project = _IntersphinxProject(name=name, target_uri=uri, locations=locations) + project = _IntersphinxProject( + name=name, target_uri=uri, locations=locations + ) except ValueError as err: - msg = __('An invalid intersphinx_mapping entry was added after normalisation.') + msg = __( + 'An invalid intersphinx_mapping entry was added after normalisation.' + ) raise ConfigError(msg) from err else: projects.append(project) @@ -223,8 +232,11 @@ def _fetch_inventory_group( or project.target_uri not in cache or cache[project.target_uri][1] < cache_time ): - LOGGER.info(__("loading intersphinx inventory '%s' from %s ..."), - project.name, _get_safe_url(inv)) + LOGGER.info( + __("loading intersphinx inventory '%s' from %s ..."), + project.name, + _get_safe_url(inv), + ) try: invdata = _fetch_inventory( @@ -245,14 +257,21 @@ def _fetch_inventory_group( if not failures: pass elif len(failures) < len(project.locations): - LOGGER.info(__('encountered some issues with some of the inventories,' - ' but they had working alternatives:')) + LOGGER.info( + __( + 'encountered some issues with some of the inventories,' + ' but they had working alternatives:' + ) + ) for fail in failures: LOGGER.info(*fail) else: issues = '\n'.join(f[0] % f[1:] for f in failures) - LOGGER.warning(__('failed to reach any of the inventories ' - 'with the following issues:') + '\n' + issues) + LOGGER.warning( + __('failed to reach any of the inventories ' 'with the following issues:') + + '\n' + + issues + ) return updated @@ -267,7 +286,7 @@ def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory: def _fetch_inventory( - *, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path, + *, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path ) -> Inventory: """Fetch, parse and return an intersphinx inventory file.""" # both *target_uri* (base URI of the links to generate) @@ -282,8 +301,12 @@ def _fetch_inventory( else: f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115 except Exception as err: - err.args = ('intersphinx inventory %r not fetchable due to %s: %s', - inv_location, err.__class__, str(err)) + err.args = ( + 'intersphinx inventory %r not fetchable due to %s: %s', + inv_location, + err.__class__, + str(err), + ) raise try: if hasattr(f, 'url'): @@ -295,17 +318,22 @@ def _fetch_inventory( if target_uri in { inv_location, path.dirname(inv_location), - path.dirname(inv_location) + '/' + path.dirname(inv_location) + '/', }: target_uri = path.dirname(new_inv_location) with f: try: invdata = InventoryFile.load(f, target_uri, posixpath.join) except ValueError as exc: - raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc + msg = f'unknown or unsupported inventory version: {exc!r}' + raise ValueError(msg) from exc except Exception as err: - err.args = ('intersphinx inventory %r not readable due to %s: %s', - inv_location, err.__class__.__name__, str(err)) + err.args = ( + 'intersphinx inventory %r not readable due to %s: %s', + inv_location, + err.__class__.__name__, + str(err), + ) raise else: return invdata @@ -373,9 +401,13 @@ def _read_from_url(url: str, *, config: Config) -> HTTPResponse: :return: data read from resource described by *url* :rtype: ``file``-like object """ - r = requests.get(url, stream=True, timeout=config.intersphinx_timeout, - _user_agent=config.user_agent, - _tls_info=(config.tls_verify, config.tls_cacerts)) + r = requests.get( + url, + stream=True, + timeout=config.intersphinx_timeout, + _user_agent=config.user_agent, + _tls_info=(config.tls_verify, config.tls_cacerts), + ) r.raise_for_status() # For inv_location / new_inv_location diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py index a816a94f257..9387d1e1096 100644 --- a/sphinx/ext/intersphinx/_resolve.py +++ b/sphinx/ext/intersphinx/_resolve.py @@ -32,9 +32,13 @@ from sphinx.util.typing import Inventory, InventoryItem, RoleFunction -def _create_element_from_result(domain: Domain, inv_name: InventoryName | None, - data: InventoryItem, - node: pending_xref, contnode: TextElement) -> nodes.reference: +def _create_element_from_result( + domain: Domain, + inv_name: InventoryName | None, + data: InventoryItem, + node: pending_xref, + contnode: TextElement, +) -> nodes.reference: proj, version, uri, dispname = data if '://' not in uri and node.get('refdoc'): # get correct path in case of subdirectories @@ -51,8 +55,11 @@ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None, # use whatever title was given, but strip prefix title = contnode.astext() if inv_name is not None and title.startswith(inv_name + ':'): - newnode.append(contnode.__class__(title[len(inv_name) + 1:], - title[len(inv_name) + 1:])) + newnode.append( + contnode.__class__( + title[len(inv_name) + 1 :], title[len(inv_name) + 1 :] + ) + ) else: newnode.append(contnode) else: @@ -62,10 +69,14 @@ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None, def _resolve_reference_in_domain_by_target( - inv_name: InventoryName | None, inventory: Inventory, - domain: Domain, objtypes: Iterable[str], - target: str, - node: pending_xref, contnode: TextElement) -> nodes.reference | None: + inv_name: InventoryName | None, + inventory: Inventory, + domain: Domain, + objtypes: Iterable[str], + target: str, + node: pending_xref, + contnode: TextElement, +) -> nodes.reference | None: for objtype in objtypes: if objtype not in inventory: # Continue if there's nothing of this kind in the inventory @@ -79,19 +90,34 @@ def _resolve_reference_in_domain_by_target( # * 'term': https://github.com/sphinx-doc/sphinx/issues/9291 # * 'label': https://github.com/sphinx-doc/sphinx/issues/12008 target_lower = target.lower() - insensitive_matches = list(filter(lambda k: k.lower() == target_lower, - inventory[objtype].keys())) + insensitive_matches = list( + filter(lambda k: k.lower() == target_lower, inventory[objtype].keys()) + ) if len(insensitive_matches) > 1: - data_items = {inventory[objtype][match] for match in insensitive_matches} + data_items = { + inventory[objtype][match] for match in insensitive_matches + } inv_descriptor = inv_name or 'main_inventory' if len(data_items) == 1: # these are duplicates; relatively innocuous - LOGGER.debug(__("inventory '%s': duplicate matches found for %s:%s"), - inv_descriptor, objtype, target, - type='intersphinx', subtype='external', location=node) + LOGGER.debug( + __("inventory '%s': duplicate matches found for %s:%s"), + inv_descriptor, + objtype, + target, + type='intersphinx', + subtype='external', + location=node, + ) else: - LOGGER.warning(__("inventory '%s': multiple matches found for %s:%s"), - inv_descriptor, objtype, target, - type='intersphinx', subtype='external', location=node) + LOGGER.warning( + __("inventory '%s': multiple matches found for %s:%s"), + inv_descriptor, + objtype, + target, + type='intersphinx', + subtype='external', + location=node, + ) if insensitive_matches: data = inventory[objtype][insensitive_matches[0]] else: @@ -106,12 +132,16 @@ def _resolve_reference_in_domain_by_target( return None -def _resolve_reference_in_domain(env: BuildEnvironment, - inv_name: InventoryName | None, inventory: Inventory, - honor_disabled_refs: bool, - domain: Domain, objtypes: Iterable[str], - node: pending_xref, contnode: TextElement, - ) -> nodes.reference | None: +def _resolve_reference_in_domain( + env: BuildEnvironment, + inv_name: InventoryName | None, + inventory: Inventory, + honor_disabled_refs: bool, + domain: Domain, + objtypes: Iterable[str], + node: pending_xref, + contnode: TextElement, +) -> nodes.reference | None: obj_types: dict[str, None] = {}.fromkeys(objtypes) # we adjust the object types for backwards compatibility @@ -129,15 +159,16 @@ def _resolve_reference_in_domain(env: BuildEnvironment, # now that the objtypes list is complete we can remove the disabled ones if honor_disabled_refs: disabled = set(env.config.intersphinx_disabled_reftypes) - obj_types = {obj_type: None - for obj_type in obj_types - if obj_type not in disabled} + obj_types = { + obj_type: None for obj_type in obj_types if obj_type not in disabled + } objtypes = [*obj_types.keys()] # without qualification - res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, - node['reftarget'], node, contnode) + res = _resolve_reference_in_domain_by_target( + inv_name, inventory, domain, objtypes, node['reftarget'], node, contnode + ) if res is not None: return res @@ -145,14 +176,19 @@ def _resolve_reference_in_domain(env: BuildEnvironment, full_qualified_name = domain.get_full_qualified_name(node) if full_qualified_name is None: return None - return _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, - full_qualified_name, node, contnode) - - -def _resolve_reference(env: BuildEnvironment, - inv_name: InventoryName | None, inventory: Inventory, - honor_disabled_refs: bool, - node: pending_xref, contnode: TextElement) -> nodes.reference | None: + return _resolve_reference_in_domain_by_target( + inv_name, inventory, domain, objtypes, full_qualified_name, node, contnode + ) + + +def _resolve_reference( + env: BuildEnvironment, + inv_name: InventoryName | None, + inventory: Inventory, + honor_disabled_refs: bool, + node: pending_xref, + contnode: TextElement, +) -> nodes.reference | None: # disabling should only be done if no inventory is given honor_disabled_refs = honor_disabled_refs and inv_name is None intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes @@ -163,13 +199,22 @@ def _resolve_reference(env: BuildEnvironment, typ = node['reftype'] if typ == 'any': for domain in env.domains.sorted(): - if honor_disabled_refs and f'{domain.name}:*' in intersphinx_disabled_reftypes: + if ( + honor_disabled_refs + and f'{domain.name}:*' in intersphinx_disabled_reftypes + ): continue objtypes: Iterable[str] = domain.object_types.keys() - res = _resolve_reference_in_domain(env, inv_name, inventory, - honor_disabled_refs, - domain, objtypes, - node, contnode) + res = _resolve_reference_in_domain( + env, + inv_name, + inventory, + honor_disabled_refs, + domain, + objtypes, + node, + contnode, + ) if res is not None: return res return None @@ -184,20 +229,28 @@ def _resolve_reference(env: BuildEnvironment, objtypes = domain.objtypes_for_role(typ) or () if not objtypes: return None - return _resolve_reference_in_domain(env, inv_name, inventory, - honor_disabled_refs, - domain, objtypes, - node, contnode) + return _resolve_reference_in_domain( + env, + inv_name, + inventory, + honor_disabled_refs, + domain, + objtypes, + node, + contnode, + ) def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool: return inv_name in InventoryAdapter(env).named_inventory -def resolve_reference_in_inventory(env: BuildEnvironment, - inv_name: InventoryName, - node: pending_xref, contnode: TextElement, - ) -> nodes.reference | None: +def resolve_reference_in_inventory( + env: BuildEnvironment, + inv_name: InventoryName, + node: pending_xref, + contnode: TextElement, +) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried in the given inventory with the target as is. @@ -205,26 +258,39 @@ def resolve_reference_in_inventory(env: BuildEnvironment, Requires ``inventory_exists(env, inv_name)``. """ assert inventory_exists(env, inv_name) - return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name], - False, node, contnode) - - -def resolve_reference_any_inventory(env: BuildEnvironment, - honor_disabled_refs: bool, - node: pending_xref, contnode: TextElement, - ) -> nodes.reference | None: + return _resolve_reference( + env, + inv_name, + InventoryAdapter(env).named_inventory[inv_name], + False, + node, + contnode, + ) + + +def resolve_reference_any_inventory( + env: BuildEnvironment, + honor_disabled_refs: bool, + node: pending_xref, + contnode: TextElement, +) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried with the target as is in any inventory. """ - return _resolve_reference(env, None, InventoryAdapter(env).main_inventory, - honor_disabled_refs, - node, contnode) - - -def resolve_reference_detect_inventory(env: BuildEnvironment, - node: pending_xref, contnode: TextElement, - ) -> nodes.reference | None: + return _resolve_reference( + env, + None, + InventoryAdapter(env).main_inventory, + honor_disabled_refs, + node, + contnode, + ) + + +def resolve_reference_detect_inventory( + env: BuildEnvironment, node: pending_xref, contnode: TextElement +) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried first with the target as is in any inventory. @@ -250,8 +316,9 @@ def resolve_reference_detect_inventory(env: BuildEnvironment, return res_inv -def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, - contnode: TextElement) -> nodes.reference | None: +def missing_reference( + app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: TextElement +) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references.""" return resolve_reference_detect_inventory(env, node, contnode) @@ -263,7 +330,11 @@ class IntersphinxDispatcher(CustomReSTDispatcher): """ def role( - self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter, + self, + role_name: str, + language_module: ModuleType, + lineno: int, + reporter: Reporter, ) -> tuple[RoleFunction, list[system_message]]: if len(role_name) > 9 and role_name.startswith(('external:', 'external+')): return IntersphinxRole(role_name), [] @@ -461,7 +532,9 @@ def is_existent_role(self, domain_name: str, role_name: str) -> bool: except ExtensionError: return False - def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]: + def invoke_role( + self, role: tuple[str, str] + ) -> tuple[list[Node], list[system_message]]: """Invoke the role described by a ``(domain, role name)`` pair.""" _deprecation_warning( __name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0) @@ -471,8 +544,15 @@ def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_me role_func = domain.role(role[1]) assert role_func is not None - return role_func(':'.join(role), self.rawtext, self.text, self.lineno, - self.inliner, self.options, self.content) + return role_func( + ':'.join(role), + self.rawtext, + self.text, + self.lineno, + self.inliner, + self.options, + self.content, + ) else: return [], [] @@ -493,13 +573,20 @@ def run(self, **kwargs: Any) -> None: inv_name = node['inventory'] if inv_name is not None: assert inventory_exists(self.env, inv_name) - newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode) + newnode = resolve_reference_in_inventory( + self.env, inv_name, node, contnode + ) else: - newnode = resolve_reference_any_inventory(self.env, False, node, contnode) + newnode = resolve_reference_any_inventory( + self.env, False, node, contnode + ) if newnode is None: typ = node['reftype'] - msg = (__('external %s:%s reference target not found: %s') % - (node['refdomain'], typ, node['reftarget'])) + msg = __('external %s:%s reference target not found: %s') % ( + node['refdomain'], + typ, + node['reftarget'], + ) LOGGER.warning(msg, location=node, type='ref', subtype=typ) node.replace_self(contnode) else: diff --git a/sphinx/ext/intersphinx/_shared.py b/sphinx/ext/intersphinx/_shared.py index 36c73786f34..44ad9562383 100644 --- a/sphinx/ext/intersphinx/_shared.py +++ b/sphinx/ext/intersphinx/_shared.py @@ -54,7 +54,7 @@ class _IntersphinxProject: 'locations': 'A tuple of local or remote targets containing ' 'the inventory data to fetch. ' 'None indicates the default inventory file name.', - } + } # fmt: skip def __init__( self, @@ -83,10 +83,12 @@ def __init__( object.__setattr__(self, 'locations', tuple(locations)) def __repr__(self) -> str: - return (f'{self.__class__.__name__}(' - f'name={self.name!r}, ' - f'target_uri={self.target_uri!r}, ' - f'locations={self.locations!r})') + return ( + f'{self.__class__.__name__}(' + f'name={self.name!r}, ' + f'target_uri={self.target_uri!r}, ' + f'locations={self.locations!r})' + ) def __eq__(self, other: object) -> bool: if not isinstance(other, _IntersphinxProject): diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 0945731702a..836dc8b864c 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -45,7 +45,7 @@ class todolist(nodes.General, nodes.Element): pass -class Todo(BaseAdmonition, SphinxDirective): # type: ignore[misc] +class Todo(BaseAdmonition, SphinxDirective): """ A todo entry, displayed (if configured) in the form of an admonition. """ diff --git a/sphinx/io.py b/sphinx/io.py index 31d64ca6d9d..7da15e1ca6a 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -143,7 +143,7 @@ def setup(self, app: Sphinx) -> None: self.transforms.remove(transform) -class SphinxDummyWriter(UnfilteredWriter): # type: ignore[misc] +class SphinxDummyWriter(UnfilteredWriter): # type: ignore[type-arg] """Dummy writer module used for generating doctree.""" supported = ('html',) # needed to keep "meta" nodes diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 92de047c569..da078b2f0f0 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -52,7 +52,9 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]: try: filename = loader.get_filename(modname) except ImportError as err: - raise PycodeError('error getting filename for %r' % modname, err) from err + raise PycodeError( + 'error getting filename for %r' % modname, err + ) from err if filename is None: # all methods for getting filename failed, so raise... raise PycodeError('no source found for module %r' % modname) @@ -70,12 +72,17 @@ def get_module_source(modname: str) -> tuple[str | None, str | None]: @classmethod def for_string( - cls: type[ModuleAnalyzer], string: str, modname: str, srcname: str = '', + cls: type[ModuleAnalyzer], + string: str, + modname: str, + srcname: str = '', ) -> ModuleAnalyzer: return cls(string, modname, srcname) @classmethod - def for_file(cls: type[ModuleAnalyzer], filename: str, modname: str) -> ModuleAnalyzer: + def for_file( + cls: type[ModuleAnalyzer], filename: str, modname: str + ) -> ModuleAnalyzer: if ('file', filename) in cls.cache: return cls.cache['file', filename] try: @@ -126,7 +133,7 @@ def analyze(self) -> None: parser.parse() self.attr_docs = {} - for (scope, comment) in parser.comments.items(): + for scope, comment in parser.comments.items(): if comment: self.attr_docs[scope] = [*comment.splitlines(), ''] else: diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 7ed107f4ab3..f6bcbda9759 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -6,36 +6,34 @@ from typing import NoReturn, overload OPERATORS: dict[type[ast.AST], str] = { - ast.Add: "+", - ast.And: "and", - ast.BitAnd: "&", - ast.BitOr: "|", - ast.BitXor: "^", - ast.Div: "/", - ast.FloorDiv: "//", - ast.Invert: "~", - ast.LShift: "<<", - ast.MatMult: "@", - ast.Mult: "*", - ast.Mod: "%", - ast.Not: "not", - ast.Pow: "**", - ast.Or: "or", - ast.RShift: ">>", - ast.Sub: "-", - ast.UAdd: "+", - ast.USub: "-", + ast.Add: '+', + ast.And: 'and', + ast.BitAnd: '&', + ast.BitOr: '|', + ast.BitXor: '^', + ast.Div: '/', + ast.FloorDiv: '//', + ast.Invert: '~', + ast.LShift: '<<', + ast.MatMult: '@', + ast.Mult: '*', + ast.Mod: '%', + ast.Not: 'not', + ast.Pow: '**', + ast.Or: 'or', + ast.RShift: '>>', + ast.Sub: '-', + ast.UAdd: '+', + ast.USub: '-', } @overload -def unparse(node: None, code: str = '') -> None: - ... +def unparse(node: None, code: str = '') -> None: ... # NoQA: E704 @overload -def unparse(node: ast.AST, code: str = '') -> str: - ... +def unparse(node: ast.AST, code: str = '') -> str: ... # NoQA: E704 def unparse(node: ast.AST | None, code: str = '') -> str | None: @@ -54,12 +52,13 @@ def __init__(self, code: str = '') -> None: def _visit_op(self, node: ast.AST) -> str: return OPERATORS[node.__class__] + for _op in OPERATORS: locals()[f'visit_{_op.__name__}'] = _visit_op def visit_arg(self, node: ast.arg) -> str: if node.annotation: - return f"{node.arg}: {self.visit(node.annotation)}" + return f'{node.arg}: {self.visit(node.annotation)}' else: return node.arg @@ -68,9 +67,9 @@ def _visit_arg_with_default(self, arg: ast.arg, default: ast.AST | None) -> str: name = self.visit(arg) if default: if arg.annotation: - name += " = %s" % self.visit(default) + name += ' = %s' % self.visit(default) else: - name += "=%s" % self.visit(default) + name += '=%s' % self.visit(default) return name def visit_arguments(self, node: ast.arguments) -> str: @@ -85,8 +84,10 @@ def visit_arguments(self, node: ast.arguments) -> str: for _ in range(len(kw_defaults), len(node.kwonlyargs)): kw_defaults.insert(0, None) - args: list[str] = [self._visit_arg_with_default(arg, defaults[i]) - for i, arg in enumerate(node.posonlyargs)] + args: list[str] = [ + self._visit_arg_with_default(arg, defaults[i]) + for i, arg in enumerate(node.posonlyargs) + ] if node.posonlyargs: args.append('/') @@ -95,7 +96,7 @@ def visit_arguments(self, node: ast.arguments) -> str: args.append(self._visit_arg_with_default(arg, defaults[i + posonlyargs])) if node.vararg: - args.append("*" + self.visit(node.vararg)) + args.append('*' + self.visit(node.vararg)) if node.kwonlyargs and not node.vararg: args.append('*') @@ -103,33 +104,33 @@ def visit_arguments(self, node: ast.arguments) -> str: args.append(self._visit_arg_with_default(arg, kw_defaults[i])) if node.kwarg: - args.append("**" + self.visit(node.kwarg)) + args.append('**' + self.visit(node.kwarg)) - return ", ".join(args) + return ', '.join(args) def visit_Attribute(self, node: ast.Attribute) -> str: - return f"{self.visit(node.value)}.{node.attr}" + return f'{self.visit(node.value)}.{node.attr}' def visit_BinOp(self, node: ast.BinOp) -> str: # Special case ``**`` to not have surrounding spaces. if isinstance(node.op, ast.Pow): - return "".join(map(self.visit, (node.left, node.op, node.right))) - return " ".join(map(self.visit, (node.left, node.op, node.right))) + return ''.join(map(self.visit, (node.left, node.op, node.right))) + return ' '.join(map(self.visit, (node.left, node.op, node.right))) def visit_BoolOp(self, node: ast.BoolOp) -> str: - op = " %s " % self.visit(node.op) + op = ' %s ' % self.visit(node.op) return op.join(self.visit(e) for e in node.values) def visit_Call(self, node: ast.Call) -> str: args = ', '.join( [self.visit(e) for e in node.args] - + [f"{k.arg}={self.visit(k.value)}" for k in node.keywords], + + [f'{k.arg}={self.visit(k.value)}' for k in node.keywords], ) - return f"{self.visit(node.func)}({args})" + return f'{self.visit(node.func)}({args})' def visit_Constant(self, node: ast.Constant) -> str: if node.value is Ellipsis: - return "..." + return '...' elif isinstance(node.value, int | float | complex): if self.code: return ast.get_source_segment(self.code, node) or repr(node.value) @@ -141,34 +142,34 @@ def visit_Constant(self, node: ast.Constant) -> str: def visit_Dict(self, node: ast.Dict) -> str: keys = (self.visit(k) for k in node.keys if k is not None) values = (self.visit(v) for v in node.values) - items = (k + ": " + v for k, v in zip(keys, values, strict=True)) - return "{" + ", ".join(items) + "}" + items = (k + ': ' + v for k, v in zip(keys, values, strict=True)) + return '{' + ', '.join(items) + '}' def visit_Lambda(self, node: ast.Lambda) -> str: - return "lambda %s: ..." % self.visit(node.args) + return 'lambda %s: ...' % self.visit(node.args) def visit_List(self, node: ast.List) -> str: - return "[" + ", ".join(self.visit(e) for e in node.elts) + "]" + return '[' + ', '.join(self.visit(e) for e in node.elts) + ']' def visit_Name(self, node: ast.Name) -> str: return node.id def visit_Set(self, node: ast.Set) -> str: - return "{" + ", ".join(self.visit(e) for e in node.elts) + "}" + return '{' + ', '.join(self.visit(e) for e in node.elts) + '}' def visit_Slice(self, node: ast.Slice) -> str: if not node.lower and not node.upper and not node.step: # Empty slice with default values -> [:] - return ":" + return ':' - start = self.visit(node.lower) if node.lower else "" - stop = self.visit(node.upper) if node.upper else "" + start = self.visit(node.lower) if node.lower else '' + stop = self.visit(node.upper) if node.upper else '' if not node.step: # Default step size -> [start:stop] - return f"{start}:{stop}" + return f'{start}:{stop}' - step = self.visit(node.step) if node.step else "" - return f"{start}:{stop}:{step}" + step = self.visit(node.step) if node.step else '' + return f'{start}:{stop}:{step}' def visit_Subscript(self, node: ast.Subscript) -> str: def is_simple_tuple(value: ast.expr) -> bool: @@ -179,25 +180,24 @@ def is_simple_tuple(value: ast.expr) -> bool: ) if is_simple_tuple(node.slice): - elts = ", ".join(self.visit(e) - for e in node.slice.elts) # type: ignore[attr-defined] - return f"{self.visit(node.value)}[{elts}]" - return f"{self.visit(node.value)}[{self.visit(node.slice)}]" + elts = ', '.join(self.visit(e) for e in node.slice.elts) # type: ignore[attr-defined] + return f'{self.visit(node.value)}[{elts}]' + return f'{self.visit(node.value)}[{self.visit(node.slice)}]' def visit_UnaryOp(self, node: ast.UnaryOp) -> str: # UnaryOp is one of {UAdd, USub, Invert, Not}, which refer to ``+x``, # ``-x``, ``~x``, and ``not x``. Only Not needs a space. if isinstance(node.op, ast.Not): - return f"{self.visit(node.op)} {self.visit(node.operand)}" - return f"{self.visit(node.op)}{self.visit(node.operand)}" + return f'{self.visit(node.op)} {self.visit(node.operand)}' + return f'{self.visit(node.op)}{self.visit(node.operand)}' def visit_Tuple(self, node: ast.Tuple) -> str: if len(node.elts) == 0: - return "()" + return '()' elif len(node.elts) == 1: - return "(%s,)" % self.visit(node.elts[0]) + return '(%s,)' % self.visit(node.elts[0]) else: - return "(" + ", ".join(self.visit(e) for e in node.elts) + ")" + return '(' + ', '.join(self.visit(e) for e in node.elts) + ')' def generic_visit(self, node: ast.AST) -> NoReturn: raise NotImplementedError('Unable to parse %s object' % type(node).__name__) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 18bb993053b..47ce2da1a57 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -63,11 +63,12 @@ def get_lvar_names(node: ast.AST, self: ast.arg | None = None) -> list[str]: return members elif node_name == 'Attribute': if ( - node.value.__class__.__name__ == 'Name' and # type: ignore[attr-defined] - self and node.value.id == self_id # type: ignore[attr-defined] + node.value.__class__.__name__ == 'Name' # type: ignore[attr-defined] + and self + and node.value.id == self_id # type: ignore[attr-defined] ): # instance variable - return ["%s" % get_lvar_names(node.attr, self)[0]] # type: ignore[attr-defined] + return ['%s' % get_lvar_names(node.attr, self)[0]] # type: ignore[attr-defined] else: raise TypeError('The assignment %r is not instance variable' % node) elif node_name == 'str': @@ -80,6 +81,7 @@ def get_lvar_names(node: ast.AST, self: ast.arg | None = None) -> list[str]: def dedent_docstring(s: str) -> str: """Remove common leading indentation from docstring.""" + def dummy() -> None: # dummy function to mock `inspect.getdoc`. pass @@ -87,16 +89,22 @@ def dummy() -> None: dummy.__doc__ = s docstring = inspect.getdoc(dummy) if docstring: - return docstring.lstrip("\r\n").rstrip("\r\n") + return docstring.lstrip('\r\n').rstrip('\r\n') else: - return "" + return '' class Token: """Better token wrapper for tokenize module.""" - def __init__(self, kind: int, value: Any, start: tuple[int, int], end: tuple[int, int], - source: str) -> None: + def __init__( + self, + kind: int, + value: Any, + start: tuple[int, int], + end: tuple[int, int], + source: str, + ) -> None: self.kind = kind self.value = value self.start = start @@ -201,7 +209,9 @@ def fetch_rvalue(self) -> list[Token]: def parse(self) -> None: """Parse the code and obtain comment after assignment.""" # skip lvalue (or whole of AnnAssign) - while (tok := self.fetch_token()) and not tok.match([OP, '='], NEWLINE, COMMENT): + while (tok := self.fetch_token()) and not tok.match( + [OP, '='], NEWLINE, COMMENT + ): assert tok assert tok is not None @@ -239,7 +249,7 @@ def __init__(self, buffers: list[str], encoding: str) -> None: def get_qualname_for(self, name: str) -> list[str] | None: """Get qualified name for given object as a list of string(s).""" if self.current_function: - if self.current_classes and self.context[-1] == "__init__": + if self.current_classes and self.context[-1] == '__init__': # store variable comments inside __init__ method of classes return self.context[:-1] + [name] else: @@ -250,31 +260,32 @@ def get_qualname_for(self, name: str) -> list[str] | None: def add_entry(self, name: str) -> None: qualname = self.get_qualname_for(name) if qualname: - self.deforders[".".join(qualname)] = next(self.counter) + self.deforders['.'.join(qualname)] = next(self.counter) def add_final_entry(self, name: str) -> None: qualname = self.get_qualname_for(name) if qualname: - self.finals.append(".".join(qualname)) + self.finals.append('.'.join(qualname)) def add_overload_entry(self, func: ast.FunctionDef) -> None: # avoid circular import problem from sphinx.util.inspect import signature_from_ast + qualname = self.get_qualname_for(func.name) if qualname: - overloads = self.overloads.setdefault(".".join(qualname), []) + overloads = self.overloads.setdefault('.'.join(qualname), []) overloads.append(signature_from_ast(func)) def add_variable_comment(self, name: str, comment: str) -> None: qualname = self.get_qualname_for(name) if qualname: - basename = ".".join(qualname[:-1]) + basename = '.'.join(qualname[:-1]) self.comments[(basename, name)] = comment def add_variable_annotation(self, name: str, annotation: ast.AST) -> None: qualname = self.get_qualname_for(name) if qualname: - basename = ".".join(qualname[:-1]) + basename = '.'.join(qualname[:-1]) self.annotations[(basename, name)] = ast_unparse(annotation) def is_final(self, decorators: list[ast.expr]) -> bool: @@ -353,7 +364,10 @@ def visit_Assign(self, node: ast.Assign) -> None: try: targets = get_assign_targets(node) varnames: list[str] = functools.reduce( - operator.iadd, [get_lvar_names(t, self=self.get_self()) for t in targets], []) + operator.iadd, + [get_lvar_names(t, self=self.get_self()) for t in targets], + [], + ) current_line = self.get_line(node.lineno) except TypeError: return # this assignment is not new definition! @@ -364,21 +378,23 @@ def visit_Assign(self, node: ast.Assign) -> None: self.add_variable_annotation(varname, node.annotation) elif hasattr(node, 'type_comment') and node.type_comment: for varname in varnames: - self.add_variable_annotation( - varname, node.type_comment) # type: ignore[arg-type] + self.add_variable_annotation(varname, node.type_comment) # type: ignore[arg-type] # check comments after assignment - parser = AfterCommentParser([current_line[node.col_offset:]] + - self.buffers[node.lineno:]) + parser = AfterCommentParser( + [current_line[node.col_offset :]] + self.buffers[node.lineno :] + ) parser.parse() if parser.comment and comment_re.match(parser.comment): for varname in varnames: - self.add_variable_comment(varname, comment_re.sub('\\1', parser.comment)) + self.add_variable_comment( + varname, comment_re.sub('\\1', parser.comment) + ) self.add_entry(varname) return # check comments before assignment - if indent_re.match(current_line[:node.col_offset]): + if indent_re.match(current_line[: node.col_offset]): comment_lines = [] for i in range(node.lineno - 1): before_line = self.get_line(node.lineno - 1 - i) @@ -404,8 +420,11 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: def visit_Expr(self, node: ast.Expr) -> None: """Handles Expr node and pick up a comment if string.""" - if (isinstance(self.previous, ast.Assign | ast.AnnAssign) and - isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)): + if ( + isinstance(self.previous, ast.Assign | ast.AnnAssign) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ): try: targets = get_assign_targets(self.previous) varnames = get_lvar_names(targets[0], self.get_self()) @@ -446,7 +465,8 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Handles FunctionDef node and set context.""" if self.current_function is None: - self.add_entry(node.name) # should be called before setting self.current_function + # should be called before setting self.current_function + self.add_entry(node.name) if self.is_final(node.decorator_list): self.add_final_entry(node.name) if self.is_overload(node.decorator_list): @@ -491,8 +511,10 @@ def parse(self) -> None: break if token == COMMENT: pass - elif token == [OP, '@'] and (self.previous is None or - self.previous.match(NEWLINE, NL, INDENT, DEDENT)): + elif token == [OP, '@'] and ( + self.previous is None + or self.previous.match(NEWLINE, NL, INDENT, DEDENT) + ): if self.decorator is None: self.decorator = token elif token.match([NAME, 'class']): @@ -522,8 +544,7 @@ def parse_definition(self, typ: str) -> None: self.indents.append((typ, funcname, start_pos)) else: # one-liner - self.add_definition(funcname, - (typ, start_pos, name.end[0])) # type: ignore[union-attr] + self.add_definition(funcname, (typ, start_pos, name.end[0])) # type: ignore[union-attr] self.context.pop() def finalize_block(self) -> None: diff --git a/sphinx/pygments_styles.py b/sphinx/pygments_styles.py index d2266421c25..55ca71b19d2 100644 --- a/sphinx/pygments_styles.py +++ b/sphinx/pygments_styles.py @@ -43,54 +43,54 @@ class PyramidStyle(Style): # work in progress... - background_color = "#f8f8f8" - default_style = "" + background_color = '#f8f8f8' + default_style = '' styles = { - Whitespace: "#bbbbbb", - Comment: "italic #60a0b0", - Comment.Preproc: "noitalic #007020", - Comment.Special: "noitalic bg:#fff0f0", - - Keyword: "bold #007020", - Keyword.Pseudo: "nobold", - Keyword.Type: "nobold #902000", - - Operator: "#666666", - Operator.Word: "bold #007020", - - Name.Builtin: "#007020", - Name.Function: "#06287e", - Name.Class: "bold #0e84b5", - Name.Namespace: "bold #0e84b5", - Name.Exception: "#007020", - Name.Variable: "#bb60d5", - Name.Constant: "#60add5", - Name.Label: "bold #002070", - Name.Entity: "bold #d55537", - Name.Attribute: "#0e84b5", - Name.Tag: "bold #062873", - Name.Decorator: "bold #555555", - - String: "#4070a0", - String.Doc: "italic", - String.Interpol: "italic #70a0d0", - String.Escape: "bold #4070a0", - String.Regex: "#235388", - String.Symbol: "#517918", - String.Other: "#c65d09", - Number: "#40a070", - - Generic.Heading: "bold #000080", - Generic.Subheading: "bold #800080", - Generic.Deleted: "#A00000", - Generic.Inserted: "#00A000", - Generic.Error: "#FF0000", - Generic.Emph: "italic", - Generic.Strong: "bold", - Generic.Prompt: "bold #c65d09", - Generic.Output: "#888", - Generic.Traceback: "#04D", - - Error: "#a40000 bg:#fbe3e4", - } + Whitespace: '#bbbbbb', + Comment: 'italic #60a0b0', + Comment.Preproc: 'noitalic #007020', + Comment.Special: 'noitalic bg:#fff0f0', + + Keyword: 'bold #007020', + Keyword.Pseudo: 'nobold', + Keyword.Type: 'nobold #902000', + + Operator: '#666666', + Operator.Word: 'bold #007020', + + Name.Builtin: '#007020', + Name.Function: '#06287e', + Name.Class: 'bold #0e84b5', + Name.Namespace: 'bold #0e84b5', + Name.Exception: '#007020', + Name.Variable: '#bb60d5', + Name.Constant: '#60add5', + Name.Label: 'bold #002070', + Name.Entity: 'bold #d55537', + Name.Attribute: '#0e84b5', + Name.Tag: 'bold #062873', + Name.Decorator: 'bold #555555', + + String: '#4070a0', + String.Doc: 'italic', + String.Interpol: 'italic #70a0d0', + String.Escape: 'bold #4070a0', + String.Regex: '#235388', + String.Symbol: '#517918', + String.Other: '#c65d09', + Number: '#40a070', + + Generic.Heading: 'bold #000080', + Generic.Subheading: 'bold #800080', + Generic.Deleted: '#A00000', + Generic.Inserted: '#00A000', + Generic.Error: '#FF0000', + Generic.Emph: 'italic', + Generic.Strong: 'bold', + Generic.Prompt: 'bold #c65d09', + Generic.Output: '#888', + Generic.Traceback: '#04D', + + Error: '#a40000 bg:#fbe3e4', + } # fmt: skip diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index e56a00d7b8a..e02e91a2e19 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -1,4 +1,5 @@ """Create a full-text search index for offline search.""" + from __future__ import annotations import dataclasses @@ -15,12 +16,13 @@ from docutils.nodes import Element, Node from sphinx import addnodes, package_dir -from sphinx.environment import BuildEnvironment from sphinx.util.index_entries import split_index_msg if TYPE_CHECKING: from collections.abc import Iterable + from sphinx.environment import BuildEnvironment + class SearchLanguage: """ @@ -52,10 +54,11 @@ class SearchLanguage: This class is used to preprocess search word which Sphinx HTML readers type, before searching index. Default implementation does nothing. """ + lang: str = '' language_name: str = '' stopwords: set[str] = set() - js_splitter_code: str = "" + js_splitter_code: str = '' js_stemmer_rawcode: str = '' js_stemmer_code = """ /** @@ -105,16 +108,14 @@ def word_filter(self, word: str) -> bool: Return true if the target word should be registered in the search index. This method is called after stemming. """ - return ( - len(word) == 0 or not ( - ((len(word) < 3) and (12353 < ord(word[0]) < 12436)) or - (ord(word[0]) < 256 and ( - word in self.stopwords - )))) + return len(word) == 0 or not ( + ((len(word) < 3) and (12353 < ord(word[0]) < 12436)) + or (ord(word[0]) < 256 and (word in self.stopwords)) + ) # SearchEnglish imported after SearchLanguage is defined due to circular import -from sphinx.search.en import SearchEnglish +from sphinx.search.en import SearchEnglish # NoQA: E402 def parse_stop_word(source: str) -> set[str]: @@ -165,10 +166,10 @@ def dumps(self, data: Any) -> str: return self.PREFIX + json.dumps(data, sort_keys=True) + self.SUFFIX def loads(self, s: str) -> Any: - data = s[len(self.PREFIX):-len(self.SUFFIX)] - if not data or not s.startswith(self.PREFIX) or not \ - s.endswith(self.SUFFIX): - raise ValueError('invalid data') + data = s[len(self.PREFIX) : -len(self.SUFFIX)] + if not data or not s.startswith(self.PREFIX) or not s.endswith(self.SUFFIX): + msg = 'invalid data' + raise ValueError(msg) return json.loads(data) def dump(self, data: Any, f: IO[str]) -> None: @@ -187,9 +188,8 @@ def _is_meta_keywords( ) -> bool: if node.get('name') == 'keywords': meta_lang = node.get('lang') - if meta_lang is None: # lang not specified - return True - elif meta_lang == lang: # matched to html_search_language + if meta_lang is None or meta_lang == lang: + # lang not specified or matched to html_search_language return True return False @@ -222,8 +222,18 @@ def dispatch_visit(self, node: Node) -> None: # Some people might put content in raw HTML that should be searched, # so we just amateurishly strip HTML tags and index the remaining # content - nodetext = re.sub(r'', '', node.astext(), flags=re.IGNORECASE|re.DOTALL) - nodetext = re.sub(r'', '', nodetext, flags=re.IGNORECASE|re.DOTALL) + nodetext = re.sub( + r'', + '', + node.astext(), + flags=re.IGNORECASE | re.DOTALL, + ) + nodetext = re.sub( + r'', + '', + nodetext, + flags=re.IGNORECASE | re.DOTALL, + ) nodetext = re.sub(r'<[^<]+?>', '', nodetext) self.found_words.extend(self.lang.split(nodetext)) raise nodes.SkipNode @@ -245,12 +255,15 @@ class IndexBuilder: Helper class that creates a search index based on the doctrees passed to the `feed` method. """ + formats = { - 'json': json, - 'pickle': pickle + 'json': json, + 'pickle': pickle, } - def __init__(self, env: BuildEnvironment, lang: str, options: dict[str, str], scoring: str) -> None: + def __init__( + self, env: BuildEnvironment, lang: str, options: dict[str, str], scoring: str + ) -> None: self.env = env # docname -> title self._titles: dict[str, str | None] = env._search_index_titles @@ -261,9 +274,13 @@ def __init__(self, env: BuildEnvironment, lang: str, options: dict[str, str], sc # stemmed words in titles -> set(docname) self._title_mapping: dict[str, set[str]] = env._search_index_title_mapping # docname -> all titles in document - self._all_titles: dict[str, list[tuple[str, str | None]]] = env._search_index_all_titles + self._all_titles: dict[str, list[tuple[str, str | None]]] = ( + env._search_index_all_titles + ) # docname -> list(index entry) - self._index_entries: dict[str, list[tuple[str, str, str]]] = env._search_index_index_entries + self._index_entries: dict[str, list[tuple[str, str, str]]] = ( + env._search_index_index_entries + ) # objtype -> index self._objtypes: dict[tuple[str, str], int] = env._search_index_objtypes # objtype index -> (domain, type, objname (localized)) @@ -290,7 +307,7 @@ def __init__(self, env: BuildEnvironment, lang: str, options: dict[str, str], sc self.js_scorer_code = fp.read().decode() else: self.js_scorer_code = '' - self.js_splitter_code = "" + self.js_splitter_code = '' def load(self, stream: IO, format: Any) -> None: """Reconstruct from frozen data.""" @@ -298,15 +315,15 @@ def load(self, stream: IO, format: Any) -> None: format = self.formats[format] frozen = format.load(stream) # if an old index is present, we treat it as not existing. - if not isinstance(frozen, dict) or \ - frozen.get('envversion') != self.env.version: - raise ValueError('old format') + if not isinstance(frozen, dict) or frozen.get('envversion') != self.env.version: + msg = 'old format' + raise ValueError(msg) index2fn = frozen['docnames'] - self._filenames = dict(zip(index2fn, frozen['filenames'])) - self._titles = dict(zip(index2fn, frozen['titles'])) + self._filenames = dict(zip(index2fn, frozen['filenames'], strict=True)) + self._titles = dict(zip(index2fn, frozen['titles'], strict=True)) self._all_titles = {} - for docname in self._titles.keys(): + for docname in self._titles: self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): for doc, titleid in doc_tuples: @@ -331,8 +348,9 @@ def dump(self, stream: IO, format: Any) -> None: format = self.formats[format] format.dump(self.freeze(), stream) - def get_objects(self, fn2index: dict[str, int] - ) -> dict[str, list[tuple[int, int, int, str, str]]]: + def get_objects( + self, fn2index: dict[str, int] + ) -> dict[str, list[tuple[int, int, int, str, str]]]: rv: dict[str, list[tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames @@ -355,8 +373,11 @@ def get_objects(self, fn2index: dict[str, int] otype = domain.object_types.get(type) if otype: # use str() to fire translation proxies - onames[typeindex] = (domain.name, type, - str(domain.get_type_name(otype))) + onames[typeindex] = ( + domain.name, + type, + str(domain.get_type_name(otype)), + ) else: onames[typeindex] = (domain.name, type, type) if anchor == fullname: @@ -368,7 +389,9 @@ def get_objects(self, fn2index: dict[str, int] plist.append((fn2index[docname], typeindex, prio, shortanchor, name)) return rv - def get_terms(self, fn2index: dict[str, int]) -> tuple[dict[str, list[int] | int], dict[str, list[int] | int]]: + def get_terms( + self, fn2index: dict[str, int] + ) -> tuple[dict[str, list[int] | int], dict[str, list[int] | int]]: """ Return a mapping of document and title terms to their corresponding sorted document IDs. @@ -377,10 +400,10 @@ def get_terms(self, fn2index: dict[str, int]) -> tuple[dict[str, list[int] | int of integers. """ rvs: tuple[dict[str, list[int] | int], dict[str, list[int] | int]] = ({}, {}) - for rv, mapping in zip(rvs, (self._mapping, self._title_mapping)): + for rv, mapping in zip(rvs, (self._mapping, self._title_mapping), strict=True): for k, v in mapping.items(): if len(v) == 1: - fn, = v + (fn,) = v if fn in fn2index: rv[k] = fn2index[fn] else: @@ -389,7 +412,7 @@ def get_terms(self, fn2index: dict[str, int]) -> tuple[dict[str, list[int] | int def freeze(self) -> dict[str, Any]: """Create a usable data structure for serializing.""" - docnames, titles = zip(*sorted(self._titles.items())) + docnames, titles = zip(*sorted(self._titles.items()), strict=True) filenames = [self._filenames.get(docname) for docname in docnames] fn2index = {f: i for (i, f) in enumerate(docnames)} terms, title_terms = self.get_terms(fn2index) @@ -406,15 +429,28 @@ def freeze(self) -> dict[str, Any]: index_entries: dict[str, list[tuple[int, str, bool]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: - index_entries.setdefault(entry.lower(), []).append((fn2index[docname], entry_id, main_entry == "main")) + index_entries.setdefault(entry.lower(), []).append(( + fn2index[docname], + entry_id, + main_entry == 'main', + )) - return dict(docnames=docnames, filenames=filenames, titles=titles, terms=terms, - objects=objects, objtypes=objtypes, objnames=objnames, - titleterms=title_terms, envversion=self.env.version, - alltitles=alltitles, indexentries=index_entries) + return { + 'docnames': docnames, + 'filenames': filenames, + 'titles': titles, + 'terms': terms, + 'objects': objects, + 'objtypes': objtypes, + 'objnames': objnames, + 'titleterms': title_terms, + 'envversion': self.env.version, + 'alltitles': alltitles, + 'indexentries': index_entries, + } def label(self) -> str: - return f"{self.lang.language_name} (code: {self.lang.lang})" + return f'{self.lang.language_name} (code: {self.lang.lang})' def prune(self, docnames: Iterable[str]) -> None: """Remove data for all docnames not in the list.""" @@ -434,7 +470,9 @@ def prune(self, docnames: Iterable[str]) -> None: for wordnames in self._title_mapping.values(): wordnames.intersection_update(docnames) - def feed(self, docname: str, filename: str, title: str, doctree: nodes.document) -> None: + def feed( + self, docname: str, filename: str, title: str, doctree: nodes.document + ) -> None: """Feed a doctree to the index.""" self._titles[docname] = title self._filenames[docname] = filename @@ -495,15 +533,22 @@ def _visit_nodes(node: nodes.Node) -> None: # Some people might put content in raw HTML that should be searched, # so we just amateurishly strip HTML tags and index the remaining # content - nodetext = re.sub(r'', '', node.astext(), - flags=re.IGNORECASE | re.DOTALL) - nodetext = re.sub(r'', '', nodetext, - flags=re.IGNORECASE | re.DOTALL) + nodetext = re.sub( + r'', + '', + node.astext(), + flags=re.IGNORECASE | re.DOTALL, + ) + nodetext = re.sub( + r'', + '', + nodetext, + flags=re.IGNORECASE | re.DOTALL, + ) nodetext = re.sub(r'<[^<]+?>', '', nodetext) word_store.words.extend(split(nodetext)) return - elif (isinstance(node, nodes.meta) - and _is_meta_keywords(node, language)): + elif isinstance(node, nodes.meta) and _is_meta_keywords(node, language): keywords = [keyword.strip() for keyword in node['content'].split(',')] word_store.words.extend(keywords) elif isinstance(node, nodes.Text): @@ -553,11 +598,16 @@ def get_js_stemmer_code(self) -> str: """Returns JS code that will be inserted into language_data.js.""" if self.lang.js_stemmer_rawcode: js_dir = path.join(package_dir, 'search', 'minified-js') - with open(path.join(js_dir, 'base-stemmer.js'), encoding='utf-8') as js_file: + with open( + path.join(js_dir, 'base-stemmer.js'), encoding='utf-8' + ) as js_file: base_js = js_file.read() - with open(path.join(js_dir, self.lang.js_stemmer_rawcode), encoding='utf-8') as js_file: + with open( + path.join(js_dir, self.lang.js_stemmer_rawcode), encoding='utf-8' + ) as js_file: language_js = js_file.read() - return ('%s\n%s\nStemmer = %sStemmer;' % - (base_js, language_js, self.lang.language_name)) + return ( + f'{base_js}\n{language_js}\nStemmer = {self.lang.language_name}Stemmer;' + ) else: return self.lang.js_stemmer_code diff --git a/sphinx/search/da.py b/sphinx/search/da.py index 47c57448533..a56114bb6ba 100644 --- a/sphinx/search/da.py +++ b/sphinx/search/da.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -danish_stopwords = parse_stop_word(''' +danish_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/danish/stop.txt og | and i | in @@ -104,7 +102,7 @@ thi | for (conj) jer | you sådan | such, like this/like that -''') +""") class SearchDanish(SearchLanguage): diff --git a/sphinx/search/de.py b/sphinx/search/de.py index dae52c9f8ef..37aa9ec8890 100644 --- a/sphinx/search/de.py +++ b/sphinx/search/de.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -german_stopwords = parse_stop_word(''' +german_stopwords = parse_stop_word(""" |source: https://snowball.tartarus.org/algorithms/german/stop.txt aber | but @@ -287,7 +285,7 @@ zur | zu + der zwar | indeed zwischen | between -''') +""") class SearchGerman(SearchLanguage): diff --git a/sphinx/search/en.py b/sphinx/search/en.py index a1f06bd3f2a..11ebb683836 100644 --- a/sphinx/search/en.py +++ b/sphinx/search/en.py @@ -2,13 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage -english_stopwords = set(""" +english_stopwords = set( + """ a and are as at be but by for @@ -18,7 +17,8 @@ such that the their then there these they this to was will with -""".split()) +""".split() +) js_porter_stemmer = """ /** diff --git a/sphinx/search/es.py b/sphinx/search/es.py index 247095b452b..5739c88172a 100644 --- a/sphinx/search/es.py +++ b/sphinx/search/es.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -spanish_stopwords = parse_stop_word(''' +spanish_stopwords = parse_stop_word(""" |source: https://snowball.tartarus.org/algorithms/spanish/stop.txt de | from, of la | the, her @@ -347,7 +345,7 @@ tenidos tenidas tened -''') +""") class SearchSpanish(SearchLanguage): diff --git a/sphinx/search/fi.py b/sphinx/search/fi.py index 5eca6e38472..f6c22264352 100644 --- a/sphinx/search/fi.py +++ b/sphinx/search/fi.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -finnish_stopwords = parse_stop_word(''' +finnish_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/finnish/stop.txt | forms of BE @@ -97,7 +95,7 @@ niin | so nyt | now itse | self -''') +""") class SearchFinnish(SearchLanguage): diff --git a/sphinx/search/fr.py b/sphinx/search/fr.py index 4d41cf4423c..7662737d6e3 100644 --- a/sphinx/search/fr.py +++ b/sphinx/search/fr.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -french_stopwords = parse_stop_word(''' +french_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/french/stop.txt au | a + le aux | a + les @@ -183,7 +181,7 @@ quelles | which sans | without soi | oneself -''') +""") class SearchFrench(SearchLanguage): diff --git a/sphinx/search/hu.py b/sphinx/search/hu.py index ccd6ebec354..5c35b16fc65 100644 --- a/sphinx/search/hu.py +++ b/sphinx/search/hu.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -hungarian_stopwords = parse_stop_word(''' +hungarian_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/hungarian/stop.txt | prepared by Anna Tordai a @@ -210,7 +208,7 @@ vele viszont volna -''') +""") class SearchHungarian(SearchLanguage): diff --git a/sphinx/search/it.py b/sphinx/search/it.py index 8436dfa5be7..60a5cf57720 100644 --- a/sphinx/search/it.py +++ b/sphinx/search/it.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -italian_stopwords = parse_stop_word(''' +italian_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/italian/stop.txt ad | a (to) before vowel al | a + il @@ -300,7 +298,7 @@ stesse stessimo stessero -''') +""") class SearchItalian(SearchLanguage): diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index 7ff663292df..65e626fdd57 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -17,12 +17,14 @@ try: import MeCab # type: ignore[import-not-found] + native_module = True except ImportError: native_module = False try: import janome.tokenizer # type: ignore[import-not-found] + janome_module = True except ImportError: janome_module = False @@ -61,7 +63,8 @@ def split(self, input: str) -> list[str]: result = self.native.parse(input) else: result = self.ctypes_libmecab.mecab_sparse_tostr( - self.ctypes_mecab, input.encode(self.dict_encode)) + self.ctypes_mecab, input.encode(self.dict_encode) + ) return result.split(' ') def init_native(self, options: dict[str, str]) -> None: @@ -89,7 +92,8 @@ def init_ctypes(self, options: dict[str, str]) -> None: if os.path.exists(lib): libpath = lib if libpath is None: - raise RuntimeError('MeCab dynamic library is not available') + msg = 'MeCab dynamic library is not available' + raise RuntimeError(msg) param = 'mecab -Owakati' dict = options.get('dict') @@ -101,11 +105,15 @@ def init_ctypes(self, options: dict[str, str]) -> None: self.ctypes_libmecab = ctypes.CDLL(libpath) self.ctypes_libmecab.mecab_new2.argtypes = (ctypes.c_char_p,) self.ctypes_libmecab.mecab_new2.restype = ctypes.c_void_p - self.ctypes_libmecab.mecab_sparse_tostr.argtypes = (ctypes.c_void_p, ctypes.c_char_p) + self.ctypes_libmecab.mecab_sparse_tostr.argtypes = ( + ctypes.c_void_p, + ctypes.c_char_p, + ) self.ctypes_libmecab.mecab_sparse_tostr.restype = ctypes.c_char_p self.ctypes_mecab = self.ctypes_libmecab.mecab_new2(param.encode(fs_enc)) if self.ctypes_mecab is None: - raise SphinxError('mecab initialization failed') + msg = 'mecab initialization failed' + raise SphinxError(msg) def __del__(self) -> None: if self.ctypes_libmecab: @@ -121,8 +129,11 @@ def __init__(self, options: dict[str, str]) -> None: def init_tokenizer(self) -> None: if not janome_module: - raise RuntimeError('Janome is not available') - self.tokenizer = janome.tokenizer.Tokenizer(udic=self.user_dict, udic_enc=self.user_dict_enc) + msg = 'Janome is not available' + raise RuntimeError(msg) + self.tokenizer = janome.tokenizer.Tokenizer( + udic=self.user_dict, udic_enc=self.user_dict_enc + ) def split(self, input: str) -> list[str]: result = ' '.join(token.surface for token in self.tokenizer.tokenize(input)) @@ -130,14 +141,18 @@ def split(self, input: str) -> list[str]: class DefaultSplitter(BaseSplitter): - patterns_ = {re.compile(pattern): value for pattern, value in { - '[一二三四五六七八九十百千万億兆]': 'M', - '[一-龠々〆ヵヶ]': 'H', - '[ぁ-ん]': 'I', - '[ァ-ヴーア-ン゙ー]': 'K', - '[a-zA-Za-zA-Z]': 'A', - '[0-90-9]': 'N', - }.items()} + patterns_ = { + re.compile(pattern): value + for pattern, value in { + '[一二三四五六七八九十百千万億兆]': 'M', + '[一-龠々〆ヵヶ]': 'H', + '[ぁ-ん]': 'I', + '[ァ-ヴーア-ン゙ー]': 'K', + '[a-zA-Za-zA-Z]': 'A', + '[0-90-9]': 'N', + }.items() + } + # fmt: off BIAS__ = -332 BC1__ = {'HH': 6, 'II': 2461, 'KH': 406, 'OH': -1378} BC2__ = {'AA': -3267, 'AI': 2744, 'AN': -878, 'HH': -4070, 'HM': -1711, @@ -398,6 +413,7 @@ class DefaultSplitter(BaseSplitter): '委': 798, '学': -960, '市': 887, '広': -695, '後': 535, '業': -697, '相': 753, '社': -507, '福': 974, '空': -822, '者': 1811, '連': 463, '郎': 1082, '1': -270, 'E1': 306, 'ル': -673, 'ン': -496} + # fmt: on # ctype_ def ctype_(self, char: str) -> str: @@ -427,18 +443,18 @@ def split(self, input: str) -> list[str]: for i in range(4, len(seg) - 3): score = self.BIAS__ - w1 = seg[i-3] - w2 = seg[i-2] - w3 = seg[i-1] + w1 = seg[i - 3] + w2 = seg[i - 2] + w3 = seg[i - 1] w4 = seg[i] - w5 = seg[i+1] - w6 = seg[i+2] - c1 = ctype[i-3] - c2 = ctype[i-2] - c3 = ctype[i-1] + w5 = seg[i + 1] + w6 = seg[i + 2] + c1 = ctype[i - 3] + c2 = ctype[i - 2] + c3 = ctype[i - 1] c4 = ctype[i] - c5 = ctype[i+1] - c6 = ctype[i+2] + c5 = ctype[i + 1] + c6 = ctype[i + 2] score += self.ts_(self.UP1__, p1) score += self.ts_(self.UP2__, p2) score += self.ts_(self.UP3__, p3) @@ -470,7 +486,7 @@ def split(self, input: str) -> list[str]: score += self.ts_(self.TC2__, c2 + c3 + c4) score += self.ts_(self.TC3__, c3 + c4 + c5) score += self.ts_(self.TC4__, c4 + c5 + c6) -# score += self.ts_(self.TC5__, c4 + c5 + c6) + # score += self.ts_(self.TC5__, c4 + c5 + c6) score += self.ts_(self.UQ1__, p1 + c1) score += self.ts_(self.UQ2__, p2 + c2) score += self.ts_(self.UQ1__, p3 + c3) @@ -501,6 +517,7 @@ class SearchJapanese(SearchLanguage): Japanese search implementation: uses no stemmer, but word splitting is quite complicated. """ + lang = 'ja' language_name = 'Japanese' diff --git a/sphinx/search/nl.py b/sphinx/search/nl.py index cb5e8c4f9a5..98f924dde86 100644 --- a/sphinx/search/nl.py +++ b/sphinx/search/nl.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -dutch_stopwords = parse_stop_word(''' +dutch_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/dutch/stop.txt de | the en | and @@ -111,7 +109,7 @@ iemand | somebody geweest | been; past participle of 'be' andere | other -''') +""") class SearchDutch(SearchLanguage): diff --git a/sphinx/search/no.py b/sphinx/search/no.py index aa7c1043baa..dfc7786d46a 100644 --- a/sphinx/search/no.py +++ b/sphinx/search/no.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -norwegian_stopwords = parse_stop_word(''' +norwegian_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/norwegian/stop.txt og | and i | in @@ -186,7 +184,7 @@ vort | become * varte | became * vart | became * -''') +""") class SearchNorwegian(SearchLanguage): diff --git a/sphinx/search/pt.py b/sphinx/search/pt.py index 0cf96109aa7..bf9b7a3a2f8 100644 --- a/sphinx/search/pt.py +++ b/sphinx/search/pt.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -portuguese_stopwords = parse_stop_word(''' +portuguese_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/portuguese/stop.txt de | of, from a | the; to, at; her @@ -245,7 +243,7 @@ teria teríamos teriam -''') +""") class SearchPortuguese(SearchLanguage): diff --git a/sphinx/search/ro.py b/sphinx/search/ro.py index f15b7a6bb12..0c00486319a 100644 --- a/sphinx/search/ro.py +++ b/sphinx/search/ro.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Set - import snowballstemmer from sphinx.search import SearchLanguage diff --git a/sphinx/search/ru.py b/sphinx/search/ru.py index d6b817ebe15..e93046cba94 100644 --- a/sphinx/search/ru.py +++ b/sphinx/search/ru.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -russian_stopwords = parse_stop_word(''' +russian_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/russian/stop.txt и | and в | in/into @@ -235,7 +233,7 @@ | можн | нужн | нельзя -''') +""") class SearchRussian(SearchLanguage): diff --git a/sphinx/search/sv.py b/sphinx/search/sv.py index b90e2276441..b4fa1bd06a2 100644 --- a/sphinx/search/sv.py +++ b/sphinx/search/sv.py @@ -2,13 +2,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict - import snowballstemmer from sphinx.search import SearchLanguage, parse_stop_word -swedish_stopwords = parse_stop_word(''' +swedish_stopwords = parse_stop_word(""" | source: https://snowball.tartarus.org/algorithms/swedish/stop.txt och | and det | it, this/that @@ -124,7 +122,7 @@ ert | your era | your vilkas | whose -''') +""") class SearchSwedish(SearchLanguage): diff --git a/sphinx/search/tr.py b/sphinx/search/tr.py index fdfc18a227e..b999e1d96d8 100644 --- a/sphinx/search/tr.py +++ b/sphinx/search/tr.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Set - import snowballstemmer from sphinx.search import SearchLanguage diff --git a/sphinx/search/zh.py b/sphinx/search/zh.py index e40c9a9fe9b..9446a7f9b31 100644 --- a/sphinx/search/zh.py +++ b/sphinx/search/zh.py @@ -11,11 +11,13 @@ try: import jieba # type: ignore[import-not-found] + JIEBA = True except ImportError: JIEBA = False -english_stopwords = set(""" +english_stopwords = set( + """ a and are as at be but by for @@ -25,7 +27,8 @@ such that the their then there these they this to was will with -""".split()) +""".split() +) js_porter_stemmer = """ /** @@ -239,8 +242,7 @@ def split(self, input: str) -> list[str]: if JIEBA: chinese = list(jieba.cut_for_search(input)) - latin1 = \ - [term.strip() for term in self.latin1_letters.findall(input)] + latin1 = [term.strip() for term in self.latin1_letters.findall(input)] self.latin_terms.extend(latin1) return chinese + latin1 @@ -252,9 +254,9 @@ def stem(self, word: str) -> str: # if not stemmed, but would be too short after being stemmed # avoids some issues with acronyms should_not_be_stemmed = ( - word in self.latin_terms and - len(word) >= 3 and - len(self.stemmer.stemWord(word.lower())) < 3 + word in self.latin_terms + and len(word) >= 3 + and len(self.stemmer.stemWord(word.lower())) < 3 ) if should_not_be_stemmed: return word.lower() diff --git a/sphinx/templates/graphviz/graphviz.css b/sphinx/templates/graphviz/graphviz.css index 027576e34d2..30f3837b62a 100644 --- a/sphinx/templates/graphviz/graphviz.css +++ b/sphinx/templates/graphviz/graphviz.css @@ -1,12 +1,5 @@ /* - * graphviz.css - * ~~~~~~~~~~~~ - * * Sphinx stylesheet -- graphviz extension. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ img.graphviz { diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index 03e38e85e86..6f1c29cdba2 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -86,7 +86,7 @@ def app_params( kwargs: dict[str, Any] = {} # to avoid stacking positional args - for info in reversed(list(request.node.iter_markers("sphinx"))): + for info in reversed(list(request.node.iter_markers('sphinx'))): pargs |= dict(enumerate(info.args)) kwargs.update(info.kwargs) @@ -203,6 +203,7 @@ def make(*args: Any, **kwargs: Any) -> SphinxTestApp: app_ = SphinxTestApp(*args, **kwargs) apps.append(app_) return app_ + yield make sys.path[:] = syspath diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py index 49f0ffa6005..9792dcb7479 100644 --- a/sphinx/testing/path.py +++ b/sphinx/testing/path.py @@ -12,9 +12,11 @@ import builtins from collections.abc import Callable -warnings.warn("'sphinx.testing.path' is deprecated. " - "Use 'os.path' or 'pathlib' instead.", - RemovedInSphinx90Warning, stacklevel=2) +warnings.warn( + "'sphinx.testing.path' is deprecated. " "Use 'os.path' or 'pathlib' instead.", + RemovedInSphinx90Warning, + stacklevel=2, +) FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() @@ -86,7 +88,7 @@ def ismount(self) -> bool: def rmtree( self, ignore_errors: bool = False, - onerror: Callable[[Callable[..., Any], str, Any], object] | None = None, + onerror: Callable[[Callable[..., Any], str, Any], object] | None = None, ) -> None: """ Removes the file or directory and any files or directories it may diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index f99442eb004..4d221133ffb 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -29,7 +29,7 @@ from docutils.nodes import Node -def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> None: +def assert_node(node: Node, cls: Any = None, xpath: str = '', **kwargs: Any) -> None: if cls: if isinstance(cls, list): assert_node(node, cls[0], xpath=xpath, **kwargs) @@ -37,36 +37,43 @@ def assert_node(node: Node, cls: Any = None, xpath: str = "", **kwargs: Any) -> if isinstance(cls[1], tuple): assert_node(node, cls[1], xpath=xpath, **kwargs) else: - assert isinstance(node, nodes.Element), \ - 'The node%s does not have any children' % xpath - assert len(node) == 1, \ - 'The node%s has %d child nodes, not one' % (xpath, len(node)) - assert_node(node[0], cls[1:], xpath=xpath + "[0]", **kwargs) + assert ( + isinstance(node, nodes.Element) + ), f'The node{xpath} does not have any children' # fmt: skip + assert ( + len(node) == 1 + ), f'The node{xpath} has {len(node)} child nodes, not one' + assert_node(node[0], cls[1:], xpath=xpath + '[0]', **kwargs) elif isinstance(cls, tuple): - assert isinstance(node, list | nodes.Element), \ - 'The node%s does not have any items' % xpath - assert len(node) == len(cls), \ - 'The node%s has %d child nodes, not %r' % (xpath, len(node), len(cls)) + assert ( + isinstance(node, list | nodes.Element) + ), f'The node{xpath} does not have any items' # fmt: skip + assert ( + len(node) == len(cls) + ), f'The node{xpath} has {len(node)} child nodes, not {len(cls)!r}' # fmt: skip for i, nodecls in enumerate(cls): - path = xpath + "[%d]" % i + path = xpath + f'[{i}]' assert_node(node[i], nodecls, xpath=path, **kwargs) elif isinstance(cls, str): assert node == cls, f'The node {xpath!r} is not {cls!r}: {node!r}' else: - assert isinstance(node, cls), \ - f'The node{xpath} is not subclass of {cls!r}: {node!r}' + assert ( + isinstance(node, cls) + ), f'The node{xpath} is not subclass of {cls!r}: {node!r}' # fmt: skip if kwargs: - assert isinstance(node, nodes.Element), \ - 'The node%s does not have any attributes' % xpath + assert ( + isinstance(node, nodes.Element) + ), f'The node{xpath} does not have any attributes' # fmt: skip for key, value in kwargs.items(): if key not in node: if (key := key.replace('_', '-')) not in node: msg = f'The node{xpath} does not have {key!r} attribute: {node!r}' raise AssertionError(msg) - assert node[key] == value, \ - f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}' + assert ( + node[key] == value + ), f'The node{xpath}[{key}] is not {value!r}: {node[key]!r}' # keep this to restrict the API usage and to have a correct return type @@ -138,7 +145,7 @@ def __init__( # but allow the stream to be /dev/null by passing verbosity=-1 status = None if quiet else StringIO() elif not isinstance(status, StringIO): - err = "%r must be an io.StringIO object, got: %s" % ('status', type(status)) + err = f"'status' must be an io.StringIO object, got: {type(status)}" raise TypeError(err) if warning is None: @@ -146,7 +153,7 @@ def __init__( # but allow the stream to be /dev/null by passing verbosity=-1 warning = None if quiet else StringIO() elif not isinstance(warning, StringIO): - err = '%r must be an io.StringIO object, got: %s' % ('warning', type(warning)) + err = f"'warning' must be an io.StringIO object, got: {type(warning)}" raise TypeError(err) self.docutils_conf_path = srcdir / 'docutils.conf' @@ -170,11 +177,21 @@ def __init__( try: super().__init__( - srcdir, confdir, outdir, doctreedir, buildername, - confoverrides=confoverrides, status=status, warning=warning, - freshenv=freshenv, warningiserror=warningiserror, tags=tags, - verbosity=verbosity, parallel=parallel, - pdb=pdb, exception_on_warning=exception_on_warning, + srcdir, + confdir, + outdir, + doctreedir, + buildername, + confoverrides=confoverrides, + status=status, + warning=warning, + freshenv=freshenv, + warningiserror=warningiserror, + tags=tags, + verbosity=verbosity, + parallel=parallel, + pdb=pdb, + exception_on_warning=exception_on_warning, ) except Exception: self.cleanup() @@ -213,7 +230,9 @@ def cleanup(self, doctrees: bool = False) -> None: def __repr__(self) -> str: return f'<{self.__class__.__name__} buildername={self._builder_name!r}>' - def build(self, force_all: bool = False, filenames: list[str] | None = None) -> None: + def build( + self, force_all: bool = False, filenames: list[str] | None = None + ) -> None: self.env._pickled_doctree_cache.clear() super().build(force_all, filenames) @@ -225,7 +244,9 @@ class SphinxTestAppWrapperForSkipBuilding(SphinxTestApp): if it has already been built and there are any output files. """ - def build(self, force_all: bool = False, filenames: list[str] | None = None) -> None: + def build( + self, force_all: bool = False, filenames: list[str] | None = None + ) -> None: if not os.listdir(self.outdir): # if listdir is empty, do build. super().build(force_all, filenames) diff --git a/sphinx/themes/agogo/layout.html b/sphinx/themes/agogo/layout.html index 9f5fabf3e13..92b7c41d48e 100644 --- a/sphinx/themes/agogo/layout.html +++ b/sphinx/themes/agogo/layout.html @@ -1,13 +1,4 @@ -{# - agogo/layout.html - ~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the agogo theme, originally written - by Andi Albrecht. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Sphinx layout template for the agogo theme. #} {%- extends "basic/layout.html" %} {% block header %} diff --git a/sphinx/themes/agogo/static/agogo.css.jinja b/sphinx/themes/agogo/static/agogo.css.jinja index 3b7a1d0b8f4..d281c744d3c 100644 --- a/sphinx/themes/agogo/static/agogo.css.jinja +++ b/sphinx/themes/agogo/static/agogo.css.jinja @@ -1,12 +1,5 @@ /* - * agogo.css_t - * ~~~~~~~~~~~ - * * Sphinx stylesheet -- agogo theme. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ * { diff --git a/sphinx/themes/basic/defindex.html b/sphinx/themes/basic/defindex.html index c614b69aa6b..f27e5ccf540 100644 --- a/sphinx/themes/basic/defindex.html +++ b/sphinx/themes/basic/defindex.html @@ -1,11 +1,4 @@ -{# - basic/defindex.html - ~~~~~~~~~~~~~~~~~~~ - - Default template for the "index" page. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. +{# Default template for the "index" page. #}{{ warn('Now base template defindex.html is deprecated.') }} {%- extends "layout.html" %} {% set title = _('Overview') %} diff --git a/sphinx/themes/basic/domainindex.html b/sphinx/themes/basic/domainindex.html index 25d7e3002d3..f8e3305cda2 100644 --- a/sphinx/themes/basic/domainindex.html +++ b/sphinx/themes/basic/domainindex.html @@ -1,12 +1,4 @@ -{# - basic/domainindex.html - ~~~~~~~~~~~~~~~~~~~~~~ - - Template for domain indices (module index, ...). - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Template for domain indices (module index, ...). #} {%- extends "layout.html" %} {% set title = indextitle %} {% block extrahead %} diff --git a/sphinx/themes/basic/genindex-single.html b/sphinx/themes/basic/genindex-single.html index 79464dad6c0..af974149003 100644 --- a/sphinx/themes/basic/genindex-single.html +++ b/sphinx/themes/basic/genindex-single.html @@ -1,12 +1,4 @@ -{# - basic/genindex-single.html - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Template for a "single" page of a split index. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Template for a "single" page of a split index. #} {% macro indexentries(firstname, links) %} {%- if links -%} diff --git a/sphinx/themes/basic/genindex-split.html b/sphinx/themes/basic/genindex-split.html index 5375365e270..c2a10dec4d2 100644 --- a/sphinx/themes/basic/genindex-split.html +++ b/sphinx/themes/basic/genindex-split.html @@ -1,12 +1,4 @@ -{# - basic/genindex-split.html - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Template for a "split" index overview page. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Template for a "split" index overview page. #} {%- extends "layout.html" %} {% set title = _('Index') %} {% block body %} diff --git a/sphinx/themes/basic/genindex.html b/sphinx/themes/basic/genindex.html index b62a9c3db1a..c39b7493ee3 100644 --- a/sphinx/themes/basic/genindex.html +++ b/sphinx/themes/basic/genindex.html @@ -1,12 +1,4 @@ -{# - basic/genindex.html - ~~~~~~~~~~~~~~~~~~~ - - Template for an "all-in-one" index. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Template for an "all-in-one" index. #} {%- extends "layout.html" %} {% set title = _('Index') %} diff --git a/sphinx/themes/basic/globaltoc.html b/sphinx/themes/basic/globaltoc.html index 9745538f29d..d9173622633 100644 --- a/sphinx/themes/basic/globaltoc.html +++ b/sphinx/themes/basic/globaltoc.html @@ -1,11 +1,3 @@ -{# - basic/globaltoc.html - ~~~~~~~~~~~~~~~~~~~~ - - Sphinx sidebar template: global table of contents. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Sphinx sidebar template: global table of contents. #}

{{ _('Table of Contents') }}

{{ toctree(includehidden=theme_globaltoc_includehidden, collapse=theme_globaltoc_collapse, maxdepth=theme_globaltoc_maxdepth) }} diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index b438b919bfc..470e71d7d54 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -1,12 +1,4 @@ -{# - basic/layout.html - ~~~~~~~~~~~~~~~~~ - - Master layout template for Sphinx themes. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Master layout template for Sphinx themes. #} {%- block doctype -%} {%- endblock %} diff --git a/sphinx/themes/basic/localtoc.html b/sphinx/themes/basic/localtoc.html index 70f3e160b37..ddebfebaeb6 100644 --- a/sphinx/themes/basic/localtoc.html +++ b/sphinx/themes/basic/localtoc.html @@ -1,12 +1,4 @@ -{# - basic/localtoc.html - ~~~~~~~~~~~~~~~~~~~ - - Sphinx sidebar template: local table of contents. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Sphinx sidebar template: local table of contents. #} {%- if display_toc %}

{{ _('Table of Contents') }}

diff --git a/sphinx/themes/basic/page.html b/sphinx/themes/basic/page.html index 502278e7f6e..19a1c876efb 100644 --- a/sphinx/themes/basic/page.html +++ b/sphinx/themes/basic/page.html @@ -1,12 +1,4 @@ -{# - basic/page.html - ~~~~~~~~~~~~~~~ - - Master template for simple pages. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Master template for simple pages. #} {%- extends "layout.html" %} {% block body %} {{ body }} diff --git a/sphinx/themes/basic/relations.html b/sphinx/themes/basic/relations.html index 7ce494f3460..f142688bb04 100644 --- a/sphinx/themes/basic/relations.html +++ b/sphinx/themes/basic/relations.html @@ -1,12 +1,4 @@ -{# - basic/relations.html - ~~~~~~~~~~~~~~~~~~~~ - - Sphinx sidebar template: relation links. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Sphinx sidebar template: relation links. #} {%- if prev %}

{{ _('Previous topic') }}

diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 8bad82a51be..0ce54c43424 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -1,12 +1,4 @@ -{# - basic/search.html - ~~~~~~~~~~~~~~~~~ - - Template for the search page. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Template for the search page. #} {%- extends "layout.html" %} {% set title = _('Search') %} {%- block scripts %} diff --git a/sphinx/themes/basic/searchbox.html b/sphinx/themes/basic/searchbox.html index 1f084bb926e..e0045b4de26 100644 --- a/sphinx/themes/basic/searchbox.html +++ b/sphinx/themes/basic/searchbox.html @@ -1,12 +1,4 @@ -{# - basic/searchbox.html - ~~~~~~~~~~~~~~~~~~~~ - - Sphinx sidebar template: quick search box. - - :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} +{# Sphinx sidebar template: quick search box. #} {%- if pagename != "search" and builder != "singlehtml" %}