summaryrefslogtreecommitdiffstats
path: root/sphinx/builders
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/builders')
-rw-r--r--sphinx/builders/__init__.py11
-rw-r--r--sphinx/builders/_epub_base.py26
-rw-r--r--sphinx/builders/changes.py15
-rw-r--r--sphinx/builders/dirhtml.py6
-rw-r--r--sphinx/builders/dummy.py5
-rw-r--r--sphinx/builders/epub3.py8
-rw-r--r--sphinx/builders/gettext.py31
-rw-r--r--sphinx/builders/html/__init__.py184
-rw-r--r--sphinx/builders/html/_assets.py32
-rw-r--r--sphinx/builders/html/transforms.py6
-rw-r--r--sphinx/builders/latex/__init__.py73
-rw-r--r--sphinx/builders/latex/nodes.py8
-rw-r--r--sphinx/builders/latex/transforms.py25
-rw-r--r--sphinx/builders/latex/util.py2
-rw-r--r--sphinx/builders/linkcheck.py113
-rw-r--r--sphinx/builders/manpage.py14
-rw-r--r--sphinx/builders/singlehtml.py9
-rw-r--r--sphinx/builders/texinfo.py32
-rw-r--r--sphinx/builders/text.py5
-rw-r--r--sphinx/builders/xml.py7
20 files changed, 365 insertions, 247 deletions
diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py
index 805ee13..ae23556 100644
--- a/sphinx/builders/__init__.py
+++ b/sphinx/builders/__init__.py
@@ -17,7 +17,7 @@ from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import UnicodeDecodeErrorHandler, get_filetype, import_object, logging, rst
from sphinx.util.build_phase import BuildPhase
-from sphinx.util.console import bold # type: ignore[attr-defined]
+from sphinx.util.console import bold
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import sphinx_domains
from sphinx.util.i18n import CatalogInfo, CatalogRepository, docname_to_domain
@@ -25,8 +25,8 @@ from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath
from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, parallel_available
# side effect: registers roles and directives
-from sphinx import directives # noqa: F401 isort:skip
-from sphinx import roles # noqa: F401 isort:skip
+from sphinx import directives # NoQA: F401 isort:skip
+from sphinx import roles # NoQA: F401 isort:skip
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
@@ -314,8 +314,7 @@ class Builder:
doccount = len(updated_docnames)
logger.info(bold(__('looking for now-outdated files... ')), nonl=True)
- for docname in self.env.check_dependents(self.app, updated_docnames):
- updated_docnames.add(docname)
+ updated_docnames.update(self.env.check_dependents(self.app, updated_docnames))
outdated = len(updated_docnames) - doccount
if outdated:
logger.info(__('%d found'), outdated)
@@ -520,7 +519,7 @@ class Builder:
doctree.settings = doctree.settings.copy()
doctree.settings.warning_stream = None
doctree.settings.env = None
- doctree.settings.record_dependencies = None # type: ignore[assignment]
+ doctree.settings.record_dependencies = None
doctree_filename = path.join(self.doctreedir, docname + '.doctree')
ensuredir(path.dirname(doctree_filename))
diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py
index f0db49b..31862e4 100644
--- a/sphinx/builders/_epub_base.py
+++ b/sphinx/builders/_epub_base.py
@@ -168,7 +168,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
self.refnodes: list[dict[str, Any]] = []
def create_build_info(self) -> BuildInfo:
- return BuildInfo(self.config, self.tags, ['html', 'epub'])
+ return BuildInfo(self.config, self.tags, frozenset({'html', 'epub'}))
def get_theme_config(self) -> tuple[str, dict]:
return self.config.epub_theme, self.config.epub_theme_options
@@ -317,7 +317,8 @@ class EpubBuilder(StandaloneHTMLBuilder):
def footnote_spot(tree: nodes.document) -> tuple[Element, int]:
"""Find or create a spot to place footnotes.
- The function returns the tuple (parent, index)."""
+ The function returns the tuple (parent, index).
+ """
# The code uses the following heuristic:
# a) place them after the last existing footnote
# b) place them after an (empty) Footnotes rubric
@@ -417,7 +418,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
path.join(self.srcdir, src), err)
continue
if self.config.epub_fix_images:
- if img.mode in ('P',):
+ if img.mode == 'P':
# See the Pillow documentation for Image.convert()
# https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.convert
img = img.convert()
@@ -480,7 +481,6 @@ class EpubBuilder(StandaloneHTMLBuilder):
"""Create a dictionary with all metadata for the content.opf
file properly escaped.
"""
-
if (source_date_epoch := os.getenv('SOURCE_DATE_EPOCH')) is not None:
time_tuple = time.gmtime(int(source_date_epoch))
else:
@@ -510,11 +510,19 @@ class EpubBuilder(StandaloneHTMLBuilder):
# files
self.files: list[str] = []
- self.ignored_files = ['.buildinfo', 'mimetype', 'content.opf',
- 'toc.ncx', 'META-INF/container.xml',
- 'Thumbs.db', 'ehthumbs.db', '.DS_Store',
- 'nav.xhtml', self.config.epub_basename + '.epub'] + \
- self.config.epub_exclude_files
+ self.ignored_files = [
+ '.buildinfo',
+ 'mimetype',
+ 'content.opf',
+ 'toc.ncx',
+ 'META-INF/container.xml',
+ 'Thumbs.db',
+ 'ehthumbs.db',
+ '.DS_Store',
+ 'nav.xhtml',
+ self.config.epub_basename + '.epub',
+ *self.config.epub_exclude_files,
+ ]
if not self.use_index:
self.ignored_files.append('genindex' + self.out_suffix)
for root, dirs, files in os.walk(self.outdir):
diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py
index 3e24e7d..b233e85 100644
--- a/sphinx/builders/changes.py
+++ b/sphinx/builders/changes.py
@@ -12,20 +12,22 @@ from sphinx.domains.changeset import ChangeSetDomain
from sphinx.locale import _, __
from sphinx.theming import HTMLThemeFactory
from sphinx.util import logging
-from sphinx.util.console import bold # type: ignore[attr-defined]
+from sphinx.util.console import bold
from sphinx.util.fileutil import copy_asset_file
from sphinx.util.osutil import ensuredir, os_path
if TYPE_CHECKING:
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
class ChangesBuilder(Builder):
"""
- Write a summary with all versionadded/changed directives.
+ Write a summary with all versionadded/changed/deprecated/removed directives.
"""
+
name = 'changes'
epilog = __('The overview file is in %(outdir)s.')
@@ -42,6 +44,7 @@ class ChangesBuilder(Builder):
'versionadded': 'added',
'versionchanged': 'changed',
'deprecated': 'deprecated',
+ 'versionremoved': 'removed',
}
def write(self, *ignored: Any) -> None:
@@ -105,7 +108,9 @@ class ChangesBuilder(Builder):
hltext = ['.. versionadded:: %s' % version,
'.. versionchanged:: %s' % version,
- '.. deprecated:: %s' % version]
+ '.. deprecated:: %s' % version,
+ '.. versionremoved:: %s' % version,
+ ]
def hl(no: int, line: str) -> str:
line = '<a name="L%s"> </a>' % no + html.escape(line)
@@ -142,7 +147,7 @@ class ChangesBuilder(Builder):
def hl(self, text: str, version: str) -> str:
text = html.escape(text)
- for directive in ('versionchanged', 'versionadded', 'deprecated'):
+ for directive in ('versionchanged', 'versionadded', 'deprecated', 'versionremoved'):
text = text.replace(f'.. {directive}:: {version}',
f'<b>.. {directive}:: {version}</b>')
return text
@@ -151,7 +156,7 @@ class ChangesBuilder(Builder):
pass
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(ChangesBuilder)
return {
diff --git a/sphinx/builders/dirhtml.py b/sphinx/builders/dirhtml.py
index 9683ee6..dbfced3 100644
--- a/sphinx/builders/dirhtml.py
+++ b/sphinx/builders/dirhtml.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from os import path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import logging
@@ -11,6 +11,7 @@ from sphinx.util.osutil import SEP, os_path
if TYPE_CHECKING:
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -21,6 +22,7 @@ class DirectoryHTMLBuilder(StandaloneHTMLBuilder):
a directory given by their pagename, so that generated URLs don't have
``.html`` in them.
"""
+
name = 'dirhtml'
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
@@ -41,7 +43,7 @@ class DirectoryHTMLBuilder(StandaloneHTMLBuilder):
return outfilename
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.setup_extension('sphinx.builders.html')
app.add_builder(DirectoryHTMLBuilder)
diff --git a/sphinx/builders/dummy.py b/sphinx/builders/dummy.py
index f025311..9c7ce83 100644
--- a/sphinx/builders/dummy.py
+++ b/sphinx/builders/dummy.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from sphinx.builders import Builder
from sphinx.locale import __
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
class DummyBuilder(Builder):
@@ -38,7 +39,7 @@ class DummyBuilder(Builder):
pass
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(DummyBuilder)
return {
diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py
index 40d3ce7..91c76e4 100644
--- a/sphinx/builders/epub3.py
+++ b/sphinx/builders/epub3.py
@@ -22,6 +22,7 @@ from sphinx.util.osutil import make_filename
if TYPE_CHECKING:
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -75,6 +76,7 @@ class Epub3Builder(_epub_base.EpubBuilder):
and META-INF/container.xml. Afterwards, all necessary files are zipped to
an epub file.
"""
+
name = 'epub'
epilog = __('The ePub file is in %(outdir)s.')
@@ -240,7 +242,7 @@ def validate_config_values(app: Sphinx) -> None:
def convert_epub_css_files(app: Sphinx, config: Config) -> None:
- """This converts string styled epub_css_files to tuple styled one."""
+ """Convert string styled epub_css_files to tuple styled one."""
epub_css_files: list[tuple[str, dict[str, Any]]] = []
for entry in config.epub_css_files:
if isinstance(entry, str):
@@ -256,11 +258,11 @@ def convert_epub_css_files(app: Sphinx, config: Config) -> None:
config.epub_css_files = epub_css_files # type: ignore[attr-defined]
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(Epub3Builder)
# config values
- app.add_config_value('epub_basename', lambda self: make_filename(self.project), False)
+ app.add_config_value('epub_basename', lambda self: make_filename(self.project), '')
app.add_config_value('epub_version', 3.0, 'epub') # experimental
app.add_config_value('epub_theme', 'epub', 'epub')
app.add_config_value('epub_theme_options', {}, 'epub')
diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py
index 0b2bede..26d0a6d 100644
--- a/sphinx/builders/gettext.py
+++ b/sphinx/builders/gettext.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import operator
import time
from codecs import open
from collections import defaultdict
@@ -16,7 +17,7 @@ from sphinx.builders import Builder
from sphinx.errors import ThemeError
from sphinx.locale import __
from sphinx.util import logging
-from sphinx.util.console import bold # type: ignore[attr-defined]
+from sphinx.util.console import bold
from sphinx.util.display import status_iterator
from sphinx.util.i18n import CatalogInfo, docname_to_domain
from sphinx.util.index_entries import split_index_msg
@@ -27,18 +28,21 @@ from sphinx.util.template import SphinxRenderer
if TYPE_CHECKING:
import os
- from collections.abc import Generator, Iterable
+ from collections.abc import Iterable, Iterator
from docutils.nodes import Element
from sphinx.application import Sphinx
+ from sphinx.config import Config
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
class Message:
"""An entry of translatable message."""
- def __init__(self, text: str, locations: list[tuple[str, int]], uuids: list[str]):
+
+ def __init__(self, text: str, locations: list[tuple[str, int]], uuids: list[str]) -> None:
self.text = text
self.locations = locations
self.uuids = uuids
@@ -64,9 +68,9 @@ class Catalog:
line = origin.line
if line is None:
line = -1
- self.metadata[msg].append((origin.source, line, origin.uid))
+ self.metadata[msg].append((origin.source, line, origin.uid)) # type: ignore[arg-type]
- def __iter__(self) -> Generator[Message, None, None]:
+ def __iter__(self) -> Iterator[Message]:
for message in self.messages:
positions = sorted({(source, line) for source, line, uuid
in self.metadata[message]})
@@ -118,6 +122,7 @@ class I18nTags(Tags):
To translate all text inside of only nodes, this class
always returns True value even if no tags are defined.
"""
+
def eval_condition(self, condition: Any) -> bool:
return True
@@ -126,6 +131,7 @@ class I18nBuilder(Builder):
"""
General i18n builder.
"""
+
name = 'i18n'
versioning_method = 'text'
use_message_catalog = False
@@ -211,6 +217,7 @@ class MessageCatalogBuilder(I18nBuilder):
"""
Builds gettext-style message catalogs (.pot files).
"""
+
name = 'gettext'
epilog = __('The message catalogs are in %(outdir)s.')
@@ -275,7 +282,7 @@ class MessageCatalogBuilder(I18nBuilder):
__("writing message catalogs... "),
"darkgreen", len(self.catalogs),
self.app.verbosity,
- lambda textdomain__: textdomain__[0]):
+ operator.itemgetter(0)):
# noop if config.gettext_compact is set
ensuredir(path.join(self.outdir, path.dirname(textdomain)))
@@ -288,7 +295,16 @@ class MessageCatalogBuilder(I18nBuilder):
pofile.write(content)
-def setup(app: Sphinx) -> dict[str, Any]:
+def _gettext_compact_validator(app: Sphinx, config: Config) -> None:
+ gettext_compact = config.gettext_compact
+ # Convert 0/1 from the command line to ``bool`` types
+ if gettext_compact == '0':
+ config.gettext_compact = False # type: ignore[attr-defined]
+ elif gettext_compact == '1':
+ config.gettext_compact = True # type: ignore[attr-defined]
+
+
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(MessageCatalogBuilder)
app.add_config_value('gettext_compact', True, 'gettext', {bool, str})
@@ -298,6 +314,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('gettext_additional_targets', [], 'env')
app.add_config_value('gettext_last_translator', 'FULL NAME <EMAIL@ADDRESS>', 'gettext')
app.add_config_value('gettext_language_team', 'LANGUAGE <LL@li.org>', 'gettext')
+ app.connect('config-inited', _gettext_compact_validator, priority=800)
return {
'version': 'builtin',
diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py
index 85067be..75b0a39 100644
--- a/sphinx/builders/html/__init__.py
+++ b/sphinx/builders/html/__init__.py
@@ -10,6 +10,7 @@ import posixpath
import re
import sys
import time
+import types
import warnings
from os import path
from typing import IO, TYPE_CHECKING, Any
@@ -49,13 +50,16 @@ from sphinx.writers.html import HTMLWriter
from sphinx.writers.html5 import HTML5Translator
if TYPE_CHECKING:
- from collections.abc import Iterable, Iterator, Sequence
+ from collections.abc import Iterable, Iterator, Set
from docutils.nodes import Node
+ from docutils.readers import Reader
from sphinx.application import Sphinx
+ from sphinx.config import _ConfigRebuild
from sphinx.environment import BuildEnvironment
from sphinx.util.tags import Tags
+ from sphinx.util.typing import ExtensionMetadata
#: the filename for the inventory of objects
INVENTORY_FILENAME = 'objects.inv'
@@ -75,16 +79,20 @@ DOMAIN_INDEX_TYPE = tuple[
]
-def get_stable_hash(obj: Any) -> str:
- """
- Return a stable hash for a Python data structure. We can't just use
- the md5 of str(obj) since for example dictionary items are enumerated
- in unpredictable order due to hash randomization in newer Pythons.
+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):
- return get_stable_hash(list(obj.items()))
- elif isinstance(obj, (list, tuple)):
- obj = sorted(get_stable_hash(o) for o in obj)
+ 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()
@@ -107,7 +115,7 @@ class BuildInfo:
"""
@classmethod
- def load(cls, f: IO) -> BuildInfo:
+ def load(cls: type[BuildInfo], f: IO) -> BuildInfo:
try:
lines = f.readlines()
assert lines[0].rstrip() == '# Sphinx build info version 1'
@@ -125,17 +133,17 @@ class BuildInfo:
self,
config: Config | None = None,
tags: Tags | None = None,
- config_categories: Sequence[str] = (),
+ config_categories: Set[_ConfigRebuild] = frozenset(),
) -> None:
self.config_hash = ''
self.tags_hash = ''
if config:
values = {c.name: c.value for c in config.filter(config_categories)}
- self.config_hash = get_stable_hash(values)
+ self.config_hash = _stable_hash(values)
if tags:
- self.tags_hash = get_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
@@ -154,6 +162,7 @@ class StandaloneHTMLBuilder(Builder):
"""
Builds standalone HTML docs.
"""
+
name = 'html'
format = 'html'
epilog = __('The HTML pages are in %(outdir)s.')
@@ -192,7 +201,7 @@ class StandaloneHTMLBuilder(Builder):
self._js_files: list[_JavaScript] = []
# Cached Publisher for writing doctrees to HTML
- reader = docutils.readers.doctree.Reader(parser_name='restructuredtext')
+ reader: Reader = docutils.readers.doctree.Reader(parser_name='restructuredtext')
pub = Publisher(
reader=reader,
parser=reader.parser,
@@ -234,7 +243,7 @@ class StandaloneHTMLBuilder(Builder):
self.use_index = self.get_builder_config('use_index', 'html')
def create_build_info(self) -> BuildInfo:
- return BuildInfo(self.config, self.tags, ['html'])
+ return BuildInfo(self.config, self.tags, frozenset({'html'}))
def _get_translations_js(self) -> str:
candidates = [path.join(dir, self.config.language,
@@ -256,8 +265,7 @@ class StandaloneHTMLBuilder(Builder):
elif self.config.html_style is not None:
yield from self.config.html_style
elif self.theme:
- stylesheet = self.theme.get_config('theme', 'stylesheet')
- yield from map(str.strip, stylesheet.split(','))
+ yield from self.theme.stylesheets
else:
yield 'default.css'
@@ -266,9 +274,9 @@ class StandaloneHTMLBuilder(Builder):
def init_templates(self) -> None:
theme_factory = HTMLThemeFactory(self.app)
- themename, themeoptions = self.get_theme_config()
- self.theme = theme_factory.create(themename)
- self.theme_options = themeoptions.copy()
+ theme_name, theme_options = self.get_theme_config()
+ self.theme = theme_factory.create(theme_name)
+ self.theme_options = theme_options
self.create_template_bridge()
self.templates.init(self, self.theme)
@@ -277,13 +285,15 @@ class StandaloneHTMLBuilder(Builder):
if self.config.pygments_style is not None:
style = self.config.pygments_style
elif self.theme:
- style = self.theme.get_config('theme', 'pygments_style', 'none')
+ # From the ``pygments_style`` theme setting
+ style = self.theme.pygments_style_default or 'none'
else:
style = 'sphinx'
self.highlighter = PygmentsBridge('html', style)
if self.theme:
- dark_style = self.theme.get_config('theme', 'pygments_dark_style', None)
+ # From the ``pygments_dark_style`` theme setting
+ dark_style = self.theme.pygments_style_dark
else:
dark_style = None
@@ -298,8 +308,7 @@ class StandaloneHTMLBuilder(Builder):
@property
def css_files(self) -> list[_CascadingStyleSheet]:
- _deprecation_warning(__name__, f'{self.__class__.__name__}.css_files', '',
- remove=(9, 0))
+ _deprecation_warning(__name__, f'{self.__class__.__name__}.css_files', remove=(9, 0))
return self._css_files
def init_css_files(self) -> None:
@@ -325,8 +334,8 @@ class StandaloneHTMLBuilder(Builder):
@property
def script_files(self) -> list[_JavaScript]:
- _deprecation_warning(__name__, f'{self.__class__.__name__}.script_files', '',
- remove=(9, 0))
+ canonical_name = f'{self.__class__.__name__}.script_files'
+ _deprecation_warning(__name__, canonical_name, remove=(9, 0))
return self._js_files
def init_js_files(self) -> None:
@@ -429,7 +438,7 @@ class StandaloneHTMLBuilder(Builder):
doc.append(node)
self._publisher.set_source(doc)
self._publisher.publish()
- return self._publisher.writer.parts # type: ignore[union-attr]
+ return self._publisher.writer.parts
def prepare_writing(self, docnames: set[str]) -> None:
# create the search indexer
@@ -547,10 +556,11 @@ class StandaloneHTMLBuilder(Builder):
'html5_doctype': True,
}
if self.theme:
- self.globalcontext.update(
- ('theme_' + key, val) for (key, val) in
- self.theme.get_options(self.theme_options).items())
- self.globalcontext.update(self.config.html_context)
+ self.globalcontext |= {
+ f'theme_{key}': val for key, val in
+ self.theme.get_options(self.theme_options).items()
+ }
+ self.globalcontext |= self.config.html_context
def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]:
"""Collect items for the template context of a page."""
@@ -708,10 +718,8 @@ class StandaloneHTMLBuilder(Builder):
# the total count of lines for each index letter, used to distribute
# the entries into two columns
genindex = IndexEntries(self.env).create_index(self)
- indexcounts = []
- for _k, entries in genindex:
- indexcounts.append(sum(1 + len(subitems)
- for _, (_, subitems, _) in entries))
+ indexcounts = [sum(1 + len(subitems) for _, (_, subitems, _) in entries)
+ for _k, entries in genindex]
genindexcontext = {
'genindexentries': genindex,
@@ -760,7 +768,7 @@ class StandaloneHTMLBuilder(Builder):
def copy_download_files(self) -> None:
def to_relpath(f: str) -> str:
- return relative_path(self.srcdir, f) # type: ignore[arg-type]
+ return relative_path(self.srcdir, f)
# copy downloadable files
if self.env.dlfiles:
@@ -777,7 +785,7 @@ class StandaloneHTMLBuilder(Builder):
path.join(self.srcdir, src), err)
def create_pygments_style_file(self) -> None:
- """create a style file for pygments."""
+ """Create a style file for pygments."""
with open(path.join(self.outdir, '_static', 'pygments.css'), 'w',
encoding="utf-8") as f:
f.write(self.highlighter.get_stylesheet())
@@ -810,7 +818,7 @@ class StandaloneHTMLBuilder(Builder):
filename, error)
if self.theme:
- for entry in self.theme.get_theme_dirs()[::-1]:
+ for entry in reversed(self.theme.get_theme_dirs()):
copy_asset(path.join(entry, 'static'),
path.join(self.outdir, '_static'),
excluded=DOTFILES, context=context,
@@ -821,7 +829,7 @@ class StandaloneHTMLBuilder(Builder):
logger.warning(__('Failed to copy a file in html_static_file: %s: %r'),
filename, error)
- excluded = Matcher(self.config.exclude_patterns + ["**/.*"])
+ excluded = Matcher([*self.config.exclude_patterns, '**/.*'])
for entry in self.config.html_static_path:
copy_asset(path.join(self.confdir, entry),
path.join(self.outdir, '_static'),
@@ -858,7 +866,7 @@ class StandaloneHTMLBuilder(Builder):
logger.warning(__('cannot copy static file %r'), err)
def copy_extra_files(self) -> None:
- """copy html_extra_path files."""
+ """Copy html_extra_path files."""
try:
with progress_message(__('copying extra files')):
excluded = Matcher(self.config.exclude_patterns)
@@ -878,7 +886,7 @@ class StandaloneHTMLBuilder(Builder):
def cleanup(self) -> None:
# clean up theme stuff
if self.theme:
- self.theme.cleanup()
+ self.theme._cleanup()
def post_process_images(self, doctree: Node) -> None:
"""Pick the best candidate for an image and link down-scaled images to
@@ -888,7 +896,7 @@ class StandaloneHTMLBuilder(Builder):
if self.config.html_scaled_image_link and self.html_scaled_image_link:
for node in doctree.findall(nodes.image):
- if not any((key in node) for key in ['scale', 'width', 'height']):
+ if not any((key in node) for key in ('scale', 'width', 'height')):
# resizing options are not given. scaled image link is available
# only for resized images.
continue
@@ -933,7 +941,7 @@ class StandaloneHTMLBuilder(Builder):
if self.indexer is not None and title:
filename = self.env.doc2path(pagename, base=False)
metadata = self.env.metadata.get(pagename, {})
- if 'nosearch' in metadata:
+ if 'no-search' in metadata or 'nosearch' in metadata:
self.indexer.feed(pagename, filename, '', new_document(''))
else:
self.indexer.feed(pagename, filename, title, doctree)
@@ -953,27 +961,11 @@ class StandaloneHTMLBuilder(Builder):
def has_wildcard(pattern: str) -> bool:
return any(char in pattern for char in '*?[')
- sidebars = None
matched = None
customsidebar = None
# default sidebars settings for selected theme
- if self.theme.name == 'alabaster':
- # provide default settings for alabaster (for compatibility)
- # Note: this will be removed before Sphinx-2.0
- try:
- # get default sidebars settings from alabaster (if defined)
- theme_default_sidebars = self.theme.config.get('theme', 'sidebars')
- if theme_default_sidebars:
- sidebars = [name.strip() for name in theme_default_sidebars.split(',')]
- except Exception:
- # fallback to better default settings
- sidebars = ['about.html', 'navigation.html', 'relations.html',
- 'searchbox.html', 'donate.html']
- else:
- theme_default_sidebars = self.theme.get_config('theme', 'sidebars', None)
- if theme_default_sidebars:
- sidebars = [name.strip() for name in theme_default_sidebars.split(',')]
+ sidebars = list(self.theme.sidebar_templates)
# user sidebar settings
html_sidebars = self.get_builder_config('sidebars', 'html')
@@ -992,7 +984,7 @@ class StandaloneHTMLBuilder(Builder):
matched = pattern
sidebars = patsidebars
- if sidebars is None:
+ if len(sidebars) == 0:
# keep defaults
pass
@@ -1040,9 +1032,7 @@ class StandaloneHTMLBuilder(Builder):
return True
if name == 'search' and self.search:
return True
- if name == 'genindex' and self.get_builder_config('use_index', 'html'):
- return True
- return False
+ return name == 'genindex' and self.get_builder_config('use_index', 'html')
ctx['hasdoc'] = hasdoc
ctx['toctree'] = lambda **kwargs: self._get_local_toctree(pagename, **kwargs)
@@ -1055,13 +1045,16 @@ class StandaloneHTMLBuilder(Builder):
outdir = self.app.outdir
def css_tag(css: _CascadingStyleSheet) -> str:
- attrs = []
- for key, value in css.attributes.items():
- if value is not None:
- attrs.append(f'{key}="{html.escape(value, quote=True)}"')
+ attrs = [f'{key}="{html.escape(value, quote=True)}"'
+ for key, value in css.attributes.items()
+ if value is not None]
uri = pathto(os.fspath(css.filename), resource=True)
- if checksum := _file_checksum(outdir, css.filename):
- uri += f'?v={checksum}'
+ # the EPUB format does not allow the use of query components
+ # the Windows help compiler requires that css links
+ # don't have a query component
+ if self.name not in {'epub', 'htmlhelp'}:
+ if checksum := _file_checksum(outdir, css.filename):
+ uri += f'?v={checksum}'
return f'<link {" ".join(sorted(attrs))} href="{uri}" />'
ctx['css_tag'] = css_tag
@@ -1071,13 +1064,10 @@ class StandaloneHTMLBuilder(Builder):
# str value (old styled)
return f'<script src="{pathto(js, resource=True)}"></script>'
- attrs = []
body = js.attributes.get('body', '')
- for key, value in js.attributes.items():
- if key == 'body':
- continue
- if value is not None:
- attrs.append(f'{key}="{html.escape(value, quote=True)}"')
+ attrs = [f'{key}="{html.escape(value, quote=True)}"'
+ for key, value in js.attributes.items()
+ if key != 'body' and value is not None]
if not js.filename:
if attrs:
@@ -1091,8 +1081,10 @@ class StandaloneHTMLBuilder(Builder):
# https://docs.mathjax.org/en/v2.7-latest/configuration.html#considerations-for-using-combined-configuration-files
# https://github.com/sphinx-doc/sphinx/issues/11658
pass
- elif checksum := _file_checksum(outdir, js.filename):
- uri += f'?v={checksum}'
+ # the EPUB format does not allow the use of query components
+ elif self.name != 'epub':
+ if checksum := _file_checksum(outdir, js.filename):
+ uri += f'?v={checksum}'
if attrs:
return f'<script {" ".join(sorted(attrs))} src="{uri}"></script>'
return f'<script src="{uri}"></script>'
@@ -1182,7 +1174,7 @@ class StandaloneHTMLBuilder(Builder):
def convert_html_css_files(app: Sphinx, config: Config) -> None:
- """This converts string styled html_css_files to tuple styled one."""
+ """Convert string styled html_css_files to tuple styled one."""
html_css_files: list[tuple[str, dict]] = []
for entry in config.html_css_files:
if isinstance(entry, str):
@@ -1205,7 +1197,7 @@ def _format_modified_time(timestamp: float) -> str:
def convert_html_js_files(app: Sphinx, config: Config) -> None:
- """This converts string styled html_js_files to tuple styled one."""
+ """Convert string styled html_js_files to tuple styled one."""
html_js_files: list[tuple[str, dict]] = []
for entry in config.html_js_files:
if isinstance(entry, str):
@@ -1302,7 +1294,7 @@ def error_on_html_4(_app: Sphinx, config: Config) -> None:
))
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
# builders
app.add_builder(StandaloneHTMLBuilder)
@@ -1310,21 +1302,20 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_theme', 'alabaster', 'html')
app.add_config_value('html_theme_path', [], 'html')
app.add_config_value('html_theme_options', {}, 'html')
- app.add_config_value('html_title',
- lambda self: _('%s %s documentation') % (self.project, self.release),
- 'html', [str])
+ app.add_config_value(
+ 'html_title', lambda c: _('%s %s documentation') % (c.project, c.release), 'html', str)
app.add_config_value('html_short_title', lambda self: self.html_title, 'html')
- app.add_config_value('html_style', None, 'html', [list, str])
- app.add_config_value('html_logo', None, 'html', [str])
- app.add_config_value('html_favicon', None, 'html', [str])
+ app.add_config_value('html_style', None, 'html', {list, str})
+ app.add_config_value('html_logo', None, 'html', str)
+ app.add_config_value('html_favicon', None, 'html', str)
app.add_config_value('html_css_files', [], 'html')
app.add_config_value('html_js_files', [], 'html')
app.add_config_value('html_static_path', [], 'html')
app.add_config_value('html_extra_path', [], 'html')
- app.add_config_value('html_last_updated_fmt', None, 'html', [str])
+ app.add_config_value('html_last_updated_fmt', None, 'html', str)
app.add_config_value('html_sidebars', {}, 'html')
app.add_config_value('html_additional_pages', {}, 'html')
- app.add_config_value('html_domain_indices', True, 'html', [list])
+ app.add_config_value('html_domain_indices', True, 'html', list)
app.add_config_value('html_permalinks', True, 'html')
app.add_config_value('html_permalinks_icon', '¶', 'html')
app.add_config_value('html_use_index', True, 'html')
@@ -1333,8 +1324,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_show_sourcelink', True, 'html')
app.add_config_value('html_sourcelink_suffix', '.txt', 'html')
app.add_config_value('html_use_opensearch', '', 'html')
- app.add_config_value('html_file_suffix', None, 'html', [str])
- app.add_config_value('html_link_suffix', None, 'html', [str])
+ app.add_config_value('html_file_suffix', None, 'html', str)
+ app.add_config_value('html_link_suffix', None, 'html', str)
app.add_config_value('html_show_copyright', True, 'html')
app.add_config_value('html_show_search_summary', True, 'html')
app.add_config_value('html_show_sphinx', True, 'html')
@@ -1342,12 +1333,13 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_output_encoding', 'utf-8', 'html')
app.add_config_value('html_compact_lists', True, 'html')
app.add_config_value('html_secnumber_suffix', '. ', 'html')
- app.add_config_value('html_search_language', None, 'html', [str])
+ app.add_config_value('html_search_language', None, 'html', str)
app.add_config_value('html_search_options', {}, 'html')
app.add_config_value('html_search_scorer', '', '')
app.add_config_value('html_scaled_image_link', True, 'html')
app.add_config_value('html_baseurl', '', 'html')
- app.add_config_value('html_codeblock_linenos_style', 'inline', 'html', # RemovedInSphinx70Warning # noqa: E501
+ # removal is indefinitely on hold (ref: https://github.com/sphinx-doc/sphinx/issues/10265)
+ app.add_config_value('html_codeblock_linenos_style', 'inline', 'html',
ENUM('table', 'inline'))
app.add_config_value('html_math_renderer', None, 'env')
app.add_config_value('html4_writer', False, 'html')
@@ -1380,14 +1372,14 @@ def setup(app: Sphinx) -> dict[str, Any]:
}
-# deprecated name -> (object to return, canonical path or empty string)
-_DEPRECATED_OBJECTS = {
+# deprecated name -> (object to return, canonical path or empty string, removal version)
+_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
'Stylesheet': (_CascadingStyleSheet, 'sphinx.builders.html._assets._CascadingStyleSheet', (9, 0)), # NoQA: E501
'JavaScript': (_JavaScript, 'sphinx.builders.html._assets._JavaScript', (9, 0)),
}
-def __getattr__(name):
+def __getattr__(name: str) -> Any:
if name not in _DEPRECATED_OBJECTS:
msg = f'module {__name__!r} has no attribute {name!r}'
raise AttributeError(msg)
diff --git a/sphinx/builders/html/_assets.py b/sphinx/builders/html/_assets.py
index a72c500..699a160 100644
--- a/sphinx/builders/html/_assets.py
+++ b/sphinx/builders/html/_assets.py
@@ -3,7 +3,7 @@ from __future__ import annotations
import os
import warnings
import zlib
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, NoReturn
from sphinx.deprecation import RemovedInSphinx90Warning
from sphinx.errors import ThemeError
@@ -27,15 +27,15 @@ class _CascadingStyleSheet:
) -> None:
object.__setattr__(self, 'filename', filename)
object.__setattr__(self, 'priority', priority)
- object.__setattr__(self, 'attributes', {'rel': rel, 'type': type, **attributes})
+ object.__setattr__(self, 'attributes', {'rel': rel, 'type': type} | attributes)
- def __str__(self):
+ def __str__(self) -> str:
attr = ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
return (f'{self.__class__.__name__}({self.filename!r}, '
f'priority={self.priority}, '
f'{attr})')
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
if isinstance(other, str):
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
@@ -46,23 +46,23 @@ class _CascadingStyleSheet:
and self.priority == other.priority
and self.attributes == other.attributes)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
- def __setattr__(self, key, value):
+ def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
- def __delattr__(self, key):
+ def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
- def __getattr__(self, key):
+ def __getattr__(self, key: str) -> str:
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
return getattr(os.fspath(self.filename), key)
- def __getitem__(self, key):
+ def __getitem__(self, key: int | slice) -> str:
warnings.warn('The str interface for _CascadingStyleSheet objects is deprecated. '
'Use css.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
return os.fspath(self.filename)[key]
@@ -83,7 +83,7 @@ class _JavaScript:
object.__setattr__(self, 'priority', priority)
object.__setattr__(self, 'attributes', attributes)
- def __str__(self):
+ def __str__(self) -> str:
attr = ''
if self.attributes:
attr = ', ' + ', '.join(f'{k}={v!r}' for k, v in self.attributes.items())
@@ -91,7 +91,7 @@ class _JavaScript:
f'priority={self.priority}'
f'{attr})')
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
if isinstance(other, str):
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
@@ -102,23 +102,23 @@ class _JavaScript:
and self.priority == other.priority
and self.attributes == other.attributes)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash((self.filename, self.priority, *sorted(self.attributes.items())))
- def __setattr__(self, key, value):
+ def __setattr__(self, key: str, value: Any) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
- def __delattr__(self, key):
+ def __delattr__(self, key: str) -> NoReturn:
msg = f'{self.__class__.__name__} is immutable'
raise AttributeError(msg)
- def __getattr__(self, key):
+ def __getattr__(self, key: str) -> str:
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
return getattr(os.fspath(self.filename), key)
- def __getitem__(self, key):
+ def __getitem__(self, key: int | slice) -> str:
warnings.warn('The str interface for _JavaScript objects is deprecated. '
'Use js.filename instead.', RemovedInSphinx90Warning, stacklevel=2)
return os.fspath(self.filename)[key]
diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py
index 18a8d38..a36588c 100644
--- a/sphinx/builders/html/transforms.py
+++ b/sphinx/builders/html/transforms.py
@@ -12,6 +12,7 @@ from sphinx.util.nodes import NodeMatcher
if TYPE_CHECKING:
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
class KeyboardTransform(SphinxPostTransform):
@@ -31,6 +32,7 @@ class KeyboardTransform(SphinxPostTransform):
<literal class="kbd">
x
"""
+
default_priority = 400
formats = ('html',)
pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)')
@@ -46,7 +48,7 @@ class KeyboardTransform(SphinxPostTransform):
matcher = NodeMatcher(nodes.literal, classes=["kbd"])
# this list must be pre-created as during iteration new nodes
# are added which match the condition in the NodeMatcher.
- for node in list(self.document.findall(matcher)): # type: nodes.literal
+ for node in list(matcher.findall(self.document)):
parts = self.pattern.split(node[-1].astext())
if len(parts) == 1 or self.is_multiwords_key(parts):
continue
@@ -76,7 +78,7 @@ class KeyboardTransform(SphinxPostTransform):
return False
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_post_transform(KeyboardTransform)
return {
diff --git a/sphinx/builders/latex/__init__.py b/sphinx/builders/latex/__init__.py
index 3ece571..2b176f9 100644
--- a/sphinx/builders/latex/__init__.py
+++ b/sphinx/builders/latex/__init__.py
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from docutils.frontend import OptionParser
-import sphinx.builders.latex.nodes # noqa: F401,E501 # Workaround: import this before writer to avoid ImportError
+import sphinx.builders.latex.nodes # NoQA: F401,E501 # Workaround: import this before writer to avoid ImportError
from sphinx import addnodes, highlighting, package_dir
from sphinx.builders import Builder
from sphinx.builders.latex.constants import ADDITIONAL_SETTINGS, DEFAULT_SETTINGS, SHORTHANDOFF
@@ -20,7 +20,7 @@ from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri, SphinxError
from sphinx.locale import _, __
from sphinx.util import logging, texescape
-from sphinx.util.console import bold, darkgreen # type: ignore[attr-defined]
+from sphinx.util.console import bold, darkgreen
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import SphinxFileOutput, new_document
from sphinx.util.fileutil import copy_asset_file
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
XINDY_LANG_OPTIONS = {
# language codes from docutils.writers.latex2e.Babel
@@ -108,6 +109,7 @@ class LaTeXBuilder(Builder):
"""
Builds LaTeX output to create PDF.
"""
+
name = 'latex'
format = 'latex'
epilog = __('The LaTeX files are in %(outdir)s.')
@@ -215,15 +217,18 @@ class LaTeXBuilder(Builder):
if self.context['latex_engine'] == 'pdflatex':
if not self.babel.uses_cyrillic():
if 'X2' in self.context['fontenc']:
- self.context['substitutefont'] = '\\usepackage{substitutefont}'
+ self.context['substitutefont'] = ('\\usepackage'
+ '{sphinxpackagesubstitutefont}')
self.context['textcyrillic'] = ('\\usepackage[Xtwo]'
'{sphinxpackagecyrillic}')
elif 'T2A' in self.context['fontenc']:
- self.context['substitutefont'] = '\\usepackage{substitutefont}'
+ self.context['substitutefont'] = ('\\usepackage'
+ '{sphinxpackagesubstitutefont}')
self.context['textcyrillic'] = ('\\usepackage[TtwoA]'
'{sphinxpackagecyrillic}')
if 'LGR' in self.context['fontenc']:
- self.context['substitutefont'] = '\\usepackage{substitutefont}'
+ self.context['substitutefont'] = ('\\usepackage'
+ '{sphinxpackagesubstitutefont}')
else:
self.context['textgreek'] = ''
if self.context['substitutefont'] == '':
@@ -338,7 +343,7 @@ class LaTeXBuilder(Builder):
def assemble_doctree(
self, indexfile: str, toctree_only: bool, appendices: list[str],
) -> nodes.document:
- self.docnames = set([indexfile] + appendices)
+ self.docnames = {indexfile, *appendices}
logger.info(darkgreen(indexfile) + " ", nonl=True)
tree = self.env.get_doctree(indexfile)
tree['docname'] = indexfile
@@ -371,9 +376,11 @@ class LaTeXBuilder(Builder):
newnodes: list[Node] = [nodes.emphasis(sectname, sectname)]
for subdir, title in self.titles:
if docname.startswith(subdir):
- newnodes.append(nodes.Text(_(' (in ')))
- newnodes.append(nodes.emphasis(title, title))
- newnodes.append(nodes.Text(')'))
+ newnodes.extend((
+ nodes.Text(_(' (in ')),
+ nodes.emphasis(title, title),
+ nodes.Text(')'),
+ ))
break
else:
pass
@@ -386,7 +393,7 @@ class LaTeXBuilder(Builder):
@progress_message(__('copying TeX support files'))
def copy_support_files(self) -> None:
- """copy TeX support files from texinputs."""
+ """Copy TeX support files from texinputs."""
# configure usage of xindy (impacts Makefile and latexmkrc)
# FIXME: convert this rather to a confval with suitable default
# according to language ? but would require extra documentation
@@ -476,7 +483,7 @@ def install_packages_for_ja(app: Sphinx) -> None:
def default_latex_engine(config: Config) -> str:
- """ Better default latex_engine settings for specific languages. """
+ """Better default latex_engine settings for specific languages."""
if config.language == 'ja':
return 'uplatex'
if config.language.startswith('zh'):
@@ -487,7 +494,7 @@ def default_latex_engine(config: Config) -> str:
def default_latex_docclass(config: Config) -> dict[str, str]:
- """ Better default latex_docclass settings for specific languages. """
+ """Better default latex_docclass settings for specific languages."""
if config.language == 'ja':
if config.latex_engine == 'uplatex':
return {'manual': 'ujbook',
@@ -500,12 +507,12 @@ def default_latex_docclass(config: Config) -> dict[str, str]:
def default_latex_use_xindy(config: Config) -> bool:
- """ Better default latex_use_xindy settings for specific engines. """
+ """Better default latex_use_xindy settings for specific engines."""
return config.latex_engine in {'xelatex', 'lualatex'}
def default_latex_documents(config: Config) -> list[tuple[str, str, str, str, str]]:
- """ Better default latex_documents settings. """
+ """Better default latex_documents settings."""
project = texescape.escape(config.project, config.latex_engine)
author = texescape.escape(config.author, config.latex_engine)
return [(config.root_doc,
@@ -515,7 +522,7 @@ def default_latex_documents(config: Config) -> list[tuple[str, str, str, str, st
config.latex_theme)]
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.setup_extension('sphinx.builders.latex.transforms')
app.add_builder(LaTeXBuilder)
@@ -523,26 +530,26 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.connect('config-inited', validate_latex_theme_options, priority=800)
app.connect('builder-inited', install_packages_for_ja)
- app.add_config_value('latex_engine', default_latex_engine, False,
+ app.add_config_value('latex_engine', default_latex_engine, '',
ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex'))
- app.add_config_value('latex_documents', default_latex_documents, False)
- app.add_config_value('latex_logo', None, False, [str])
- app.add_config_value('latex_appendices', [], False)
- app.add_config_value('latex_use_latex_multicolumn', False, False)
- app.add_config_value('latex_use_xindy', default_latex_use_xindy, False, [bool])
- app.add_config_value('latex_toplevel_sectioning', None, False,
+ app.add_config_value('latex_documents', default_latex_documents, '')
+ app.add_config_value('latex_logo', None, '', str)
+ app.add_config_value('latex_appendices', [], '')
+ app.add_config_value('latex_use_latex_multicolumn', False, '')
+ app.add_config_value('latex_use_xindy', default_latex_use_xindy, '', bool)
+ app.add_config_value('latex_toplevel_sectioning', None, '',
ENUM(None, 'part', 'chapter', 'section'))
- app.add_config_value('latex_domain_indices', True, False, [list])
- app.add_config_value('latex_show_urls', 'no', False)
- app.add_config_value('latex_show_pagerefs', False, False)
- app.add_config_value('latex_elements', {}, False)
- app.add_config_value('latex_additional_files', [], False)
- app.add_config_value('latex_table_style', ['booktabs', 'colorrows'], False, [list])
- app.add_config_value('latex_theme', 'manual', False, [str])
- app.add_config_value('latex_theme_options', {}, False)
- app.add_config_value('latex_theme_path', [], False, [list])
-
- app.add_config_value('latex_docclass', default_latex_docclass, False)
+ app.add_config_value('latex_domain_indices', True, '', list)
+ app.add_config_value('latex_show_urls', 'no', '')
+ app.add_config_value('latex_show_pagerefs', False, '')
+ app.add_config_value('latex_elements', {}, '')
+ app.add_config_value('latex_additional_files', [], '')
+ app.add_config_value('latex_table_style', ['booktabs', 'colorrows'], '', list)
+ app.add_config_value('latex_theme', 'manual', '', str)
+ app.add_config_value('latex_theme_options', {}, '')
+ app.add_config_value('latex_theme_path', [], '', list)
+
+ app.add_config_value('latex_docclass', default_latex_docclass, '')
return {
'version': 'builtin',
diff --git a/sphinx/builders/latex/nodes.py b/sphinx/builders/latex/nodes.py
index 2c008b9..68b743d 100644
--- a/sphinx/builders/latex/nodes.py
+++ b/sphinx/builders/latex/nodes.py
@@ -5,26 +5,30 @@ from docutils import nodes
class captioned_literal_block(nodes.container):
"""A node for a container of literal_block having a caption."""
+
pass
class footnotemark(nodes.Inline, nodes.Referential, nodes.TextElement):
- """A node represents ``\footnotemark``."""
+ r"""A node represents ``\footnotemark``."""
+
pass
class footnotetext(nodes.General, nodes.BackLinkable, nodes.Element,
nodes.Labeled, nodes.Targetable):
- """A node represents ``\footnotetext``."""
+ r"""A node represents ``\footnotetext``."""
class math_reference(nodes.Inline, nodes.Referential, nodes.TextElement):
"""A node for a reference for equation."""
+
pass
class thebibliography(nodes.container):
"""A node for wrapping bibliographies."""
+
pass
diff --git a/sphinx/builders/latex/transforms.py b/sphinx/builders/latex/transforms.py
index ca1e4f3..83599d8 100644
--- a/sphinx/builders/latex/transforms.py
+++ b/sphinx/builders/latex/transforms.py
@@ -25,18 +25,20 @@ if TYPE_CHECKING:
from docutils.nodes import Element, Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
URI_SCHEMES = ('mailto:', 'http:', 'https:', 'ftp:')
class FootnoteDocnameUpdater(SphinxTransform):
"""Add docname to footnote and footnote_reference nodes."""
+
default_priority = 700
TARGET_NODES = (nodes.footnote, nodes.footnote_reference)
def apply(self, **kwargs: Any) -> None:
matcher = NodeMatcher(*self.TARGET_NODES)
- for node in self.document.findall(matcher): # type: Element
+ for node in matcher.findall(self.document):
node['docname'] = self.env.docname
@@ -59,6 +61,7 @@ class ShowUrlsTransform(SphinxPostTransform):
.. note:: This transform is used for integrated doctree
"""
+
default_priority = 400
formats = ('latex',)
@@ -112,7 +115,7 @@ class ShowUrlsTransform(SphinxPostTransform):
node = node.parent
try:
- source = node['source'] # type: ignore[index]
+ source = node['source']
except TypeError:
raise ValueError(__('Failed to get a docname!')) from None
raise ValueError(__('Failed to get a docname '
@@ -509,6 +512,7 @@ class BibliographyTransform(SphinxPostTransform):
<citation>
...
"""
+
default_priority = 750
formats = ('latex',)
@@ -519,7 +523,7 @@ class BibliographyTransform(SphinxPostTransform):
citations += node
if len(citations) > 0:
- self.document += citations
+ self.document += citations # type: ignore[attr-defined]
class CitationReferenceTransform(SphinxPostTransform):
@@ -528,13 +532,14 @@ class CitationReferenceTransform(SphinxPostTransform):
To handle citation reference easily on LaTeX writer, this converts
pending_xref nodes to citation_reference.
"""
+
default_priority = 5 # before ReferencesResolver
formats = ('latex',)
def run(self, **kwargs: Any) -> None:
domain = cast(CitationDomain, self.env.get_domain('citation'))
matcher = NodeMatcher(addnodes.pending_xref, refdomain='citation', reftype='ref')
- for node in self.document.findall(matcher): # type: addnodes.pending_xref
+ for node in matcher.findall(self.document):
docname, labelid, _ = domain.citations.get(node['reftarget'], ('', '', 0))
if docname:
citation_ref = nodes.citation_reference('', '', *node.children,
@@ -548,6 +553,7 @@ class MathReferenceTransform(SphinxPostTransform):
To handle math reference easily on LaTeX writer, this converts pending_xref
nodes to math_reference.
"""
+
default_priority = 5 # before ReferencesResolver
formats = ('latex',)
@@ -563,18 +569,20 @@ class MathReferenceTransform(SphinxPostTransform):
class LiteralBlockTransform(SphinxPostTransform):
"""Replace container nodes for literal_block by captioned_literal_block."""
+
default_priority = 400
formats = ('latex',)
def run(self, **kwargs: Any) -> None:
matcher = NodeMatcher(nodes.container, literal_block=True)
- for node in self.document.findall(matcher): # type: nodes.container
+ for node in matcher.findall(self.document):
newnode = captioned_literal_block('', *node.children, **node.attributes)
node.replace_self(newnode)
class DocumentTargetTransform(SphinxPostTransform):
"""Add :doc label to the first section of each document."""
+
default_priority = 400
formats = ('latex',)
@@ -586,10 +594,10 @@ class DocumentTargetTransform(SphinxPostTransform):
class IndexInSectionTitleTransform(SphinxPostTransform):
- """Move index nodes in section title to outside of the title.
+ r"""Move index nodes in section title to outside of the title.
LaTeX index macro is not compatible with some handling of section titles
- such as uppercasing done on LaTeX side (cf. fncychap handling of ``\\chapter``).
+ such as uppercasing done on LaTeX side (cf. fncychap handling of ``\chapter``).
Moving the index node to after the title node fixes that.
Before::
@@ -611,6 +619,7 @@ class IndexInSectionTitleTransform(SphinxPostTransform):
blah blah blah
...
"""
+
default_priority = 400
formats = ('latex',)
@@ -623,7 +632,7 @@ class IndexInSectionTitleTransform(SphinxPostTransform):
node.parent.insert(i + 1, index)
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_transform(FootnoteDocnameUpdater)
app.add_post_transform(SubstitutionDefinitionsRemover)
app.add_post_transform(BibliographyTransform)
diff --git a/sphinx/builders/latex/util.py b/sphinx/builders/latex/util.py
index 01597f9..aeef260 100644
--- a/sphinx/builders/latex/util.py
+++ b/sphinx/builders/latex/util.py
@@ -35,7 +35,7 @@ class ExtBabel(Babel):
return 'english' # fallback to english
def get_mainlanguage_options(self) -> str | None:
- """Return options for polyglossia's ``\\setmainlanguage``."""
+ r"""Return options for polyglossia's ``\setmainlanguage``."""
if self.use_polyglossia is False:
return None
elif self.language == 'german':
diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py
index f250958..9178458 100644
--- a/sphinx/builders/linkcheck.py
+++ b/sphinx/builders/linkcheck.py
@@ -7,6 +7,7 @@ import json
import re
import socket
import time
+import warnings
from html.parser import HTMLParser
from os import path
from queue import PriorityQueue, Queue
@@ -16,29 +17,26 @@ from urllib.parse import unquote, urlparse, urlsplit, urlunparse
from docutils import nodes
from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
+from requests.exceptions import Timeout as RequestTimeout
from sphinx.builders.dummy import DummyBuilder
+from sphinx.deprecation import RemovedInSphinx80Warning
from sphinx.locale import __
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import encode_uri, logging, requests
-from sphinx.util.console import ( # type: ignore[attr-defined]
- darkgray,
- darkgreen,
- purple,
- red,
- turquoise,
-)
+from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise
from sphinx.util.http_date import rfc1123_to_epoch
from sphinx.util.nodes import get_node_line
if TYPE_CHECKING:
- from collections.abc import Generator, Iterator
+ from collections.abc import Iterator
from typing import Any, Callable
from requests import Response
from sphinx.application import Sphinx
from sphinx.config import Config
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -56,16 +54,37 @@ class CheckExternalLinksBuilder(DummyBuilder):
"""
Checks for broken external links.
"""
+
name = 'linkcheck'
epilog = __('Look for any errors in the above output or in '
'%(outdir)s/output.txt')
def init(self) -> None:
self.broken_hyperlinks = 0
+ self.timed_out_hyperlinks = 0
self.hyperlinks: dict[str, Hyperlink] = {}
# set a timeout for non-responding servers
socket.setdefaulttimeout(5.0)
+ if not self.config.linkcheck_allow_unauthorized:
+ deprecation_msg = (
+ "The default value for 'linkcheck_allow_unauthorized' will change "
+ "from `True` in Sphinx 7.3+ to `False`, meaning that HTTP 401 "
+ "unauthorized responses will be reported as broken by default. "
+ "See https://github.com/sphinx-doc/sphinx/issues/11433 for details."
+ )
+ warnings.warn(deprecation_msg, RemovedInSphinx80Warning, stacklevel=1)
+
+ if self.config.linkcheck_report_timeouts_as_broken:
+ deprecation_msg = (
+ "The default value for 'linkcheck_report_timeouts_as_broken' will change "
+ 'to False in Sphinx 8, meaning that request timeouts '
+ "will be reported with a new 'timeout' status, instead of as 'broken'. "
+ 'This is intended to provide more detail as to the failure mode. '
+ 'See https://github.com/sphinx-doc/sphinx/issues/11868 for details.'
+ )
+ warnings.warn(deprecation_msg, RemovedInSphinx80Warning, stacklevel=1)
+
def finish(self) -> None:
checker = HyperlinkAvailabilityChecker(self.config)
logger.info('')
@@ -77,7 +96,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
for result in checker.check(self.hyperlinks):
self.process_result(result)
- if self.broken_hyperlinks:
+ if self.broken_hyperlinks or self.timed_out_hyperlinks:
self.app.statuscode = 1
def process_result(self, result: CheckResult) -> None:
@@ -104,6 +123,15 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.write_entry('local', result.docname, filename, result.lineno, result.uri)
elif result.status == 'working':
logger.info(darkgreen('ok ') + result.uri + result.message)
+ elif result.status == 'timeout':
+ if self.app.quiet or self.app.warningiserror:
+ logger.warning('timeout ' + result.uri + result.message,
+ location=(result.docname, result.lineno))
+ else:
+ logger.info(red('timeout ') + result.uri + red(' - ' + result.message))
+ self.write_entry('timeout', result.docname, filename, result.lineno,
+ result.uri + ': ' + result.message)
+ self.timed_out_hyperlinks += 1
elif result.status == 'broken':
if self.app.quiet or self.app.warningiserror:
logger.warning(__('broken link: %s (%s)'), result.uri, result.message,
@@ -206,7 +234,7 @@ class HyperlinkAvailabilityChecker:
self.to_ignore: list[re.Pattern[str]] = list(map(re.compile,
self.config.linkcheck_ignore))
- def check(self, hyperlinks: dict[str, Hyperlink]) -> Generator[CheckResult, None, None]:
+ def check(self, hyperlinks: dict[str, Hyperlink]) -> Iterator[CheckResult]:
self.invoke_threads()
total_links = 0
@@ -283,6 +311,11 @@ class HyperlinkAvailabilityCheckWorker(Thread):
self.allowed_redirects = config.linkcheck_allowed_redirects
self.retries: int = config.linkcheck_retries
self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
+ self._allow_unauthorized = config.linkcheck_allow_unauthorized
+ if config.linkcheck_report_timeouts_as_broken:
+ self._timeout_status = 'broken'
+ else:
+ self._timeout_status = 'timeout'
self.user_agent = config.user_agent
self.tls_verify = config.tls_verify
@@ -384,7 +417,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
req_url = encode_uri(req_url)
# Get auth info, if any
- for pattern, auth_info in self.auth: # noqa: B007 (false positive)
+ for pattern, auth_info in self.auth: # NoQA: B007 (false positive)
if pattern.match(uri):
break
else:
@@ -424,6 +457,9 @@ class HyperlinkAvailabilityCheckWorker(Thread):
del response
break
+ except RequestTimeout as err:
+ return self._timeout_status, str(err), 0
+
except SSLError as err:
# SSL failure; report that the link is broken.
return 'broken', str(err), 0
@@ -437,9 +473,31 @@ class HyperlinkAvailabilityCheckWorker(Thread):
except HTTPError as err:
error_message = str(err)
- # Unauthorised: the reference probably exists
+ # Unauthorized: the client did not provide required credentials
if status_code == 401:
- return 'working', 'unauthorized', 0
+ if self._allow_unauthorized:
+ deprecation_msg = (
+ "\n---\n"
+ "The linkcheck builder encountered an HTTP 401 "
+ "(unauthorized) response, and will report it as "
+ "'working' in this version of Sphinx to maintain "
+ "backwards-compatibility."
+ "\n"
+ "This logic will change in Sphinx 8.0 which will "
+ "report the hyperlink as 'broken'."
+ "\n"
+ "To explicitly continue treating unauthorized "
+ "hyperlink responses as 'working', set the "
+ "'linkcheck_allow_unauthorized' config option to "
+ "``True``."
+ "\n"
+ "See https://github.com/sphinx-doc/sphinx/issues/11433 "
+ "for details."
+ "\n---"
+ )
+ warnings.warn(deprecation_msg, RemovedInSphinx80Warning, stacklevel=1)
+ status = 'working' if self._allow_unauthorized else 'broken'
+ return status, 'unauthorized', 0
# Rate limiting; back-off if allowed, or report failure otherwise
if status_code == 429:
@@ -534,7 +592,6 @@ def _get_request_headers(
def contains_anchor(response: Response, anchor: str) -> bool:
"""Determine if an anchor is contained within an HTTP response."""
-
parser = AnchorCheckParser(unquote(anchor))
# Read file in chunks. If we find a matching anchor, we break
# the loop early in hopes not to have to download the whole thing.
@@ -607,24 +664,26 @@ def compile_linkcheck_allowed_redirects(app: Sphinx, config: Config) -> None:
app.config.linkcheck_allowed_redirects.pop(url)
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(CheckExternalLinksBuilder)
app.add_post_transform(HyperlinkCollector)
- app.add_config_value('linkcheck_ignore', [], False)
- app.add_config_value('linkcheck_exclude_documents', [], False)
- app.add_config_value('linkcheck_allowed_redirects', {}, False)
- app.add_config_value('linkcheck_auth', [], False)
- app.add_config_value('linkcheck_request_headers', {}, False)
- app.add_config_value('linkcheck_retries', 1, False)
- app.add_config_value('linkcheck_timeout', None, False, [int, float])
- app.add_config_value('linkcheck_workers', 5, False)
- app.add_config_value('linkcheck_anchors', True, False)
+ app.add_config_value('linkcheck_ignore', [], '')
+ app.add_config_value('linkcheck_exclude_documents', [], '')
+ app.add_config_value('linkcheck_allowed_redirects', {}, '')
+ app.add_config_value('linkcheck_auth', [], '')
+ app.add_config_value('linkcheck_request_headers', {}, '')
+ app.add_config_value('linkcheck_retries', 1, '')
+ app.add_config_value('linkcheck_timeout', 30, '', (int, float))
+ app.add_config_value('linkcheck_workers', 5, '')
+ app.add_config_value('linkcheck_anchors', True, '')
# Anchors starting with ! are ignored since they are
# commonly used for dynamic pages
- app.add_config_value('linkcheck_anchors_ignore', ['^!'], False)
- app.add_config_value('linkcheck_anchors_ignore_for_url', (), False, (tuple, list))
- app.add_config_value('linkcheck_rate_limit_timeout', 300.0, False)
+ app.add_config_value('linkcheck_anchors_ignore', ['^!'], '')
+ app.add_config_value('linkcheck_anchors_ignore_for_url', (), '', (tuple, list))
+ app.add_config_value('linkcheck_rate_limit_timeout', 300.0, '')
+ app.add_config_value('linkcheck_allow_unauthorized', True, '')
+ app.add_config_value('linkcheck_report_timeouts_as_broken', True, '', bool)
app.add_event('linkcheck-process-uri')
diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py
index 2d35d20..93b381d 100644
--- a/sphinx/builders/manpage.py
+++ b/sphinx/builders/manpage.py
@@ -13,7 +13,7 @@ from sphinx import addnodes
from sphinx.builders import Builder
from sphinx.locale import __
from sphinx.util import logging
-from sphinx.util.console import darkgreen # type: ignore[attr-defined]
+from sphinx.util.console import darkgreen
from sphinx.util.display import progress_message
from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import ensuredir, make_filename_from_project
@@ -22,6 +22,7 @@ from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -30,6 +31,7 @@ class ManualPageBuilder(Builder):
"""
Builds groff output in manual page format.
"""
+
name = 'man'
format = 'man'
epilog = __('The manual pages are in %(outdir)s.')
@@ -107,18 +109,18 @@ class ManualPageBuilder(Builder):
def default_man_pages(config: Config) -> list[tuple[str, str, str, list[str], int]]:
- """ Better default man_pages settings. """
+ """Better default man_pages settings."""
filename = make_filename_from_project(config.project)
return [(config.root_doc, filename, f'{config.project} {config.release}',
[config.author], 1)]
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(ManualPageBuilder)
- app.add_config_value('man_pages', default_man_pages, False)
- app.add_config_value('man_show_urls', False, False)
- app.add_config_value('man_make_section_directory', False, False)
+ app.add_config_value('man_pages', default_man_pages, '')
+ app.add_config_value('man_show_urls', False, '')
+ app.add_config_value('man_make_section_directory', False, '')
return {
'version': 'builtin',
diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py
index cd66953..efc4eaa 100644
--- a/sphinx/builders/singlehtml.py
+++ b/sphinx/builders/singlehtml.py
@@ -11,7 +11,7 @@ from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment.adapters.toctree import global_toctree_for_doc
from sphinx.locale import __
from sphinx.util import logging
-from sphinx.util.console import darkgreen # type: ignore[attr-defined]
+from sphinx.util.console import darkgreen
from sphinx.util.display import progress_message
from sphinx.util.nodes import inline_all_toctrees
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -28,6 +29,7 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
A StandaloneHTMLBuilder subclass that puts the whole document tree on one
HTML page.
"""
+
name = 'singlehtml'
epilog = __('The HTML page is in %(outdir)s.')
@@ -39,8 +41,7 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
if docname in self.env.all_docs:
# all references are on the same page...
- return self.config.root_doc + self.out_suffix + \
- '#document-' + docname
+ return '#document-' + docname
else:
# chances are this is a html_additional_page
return docname + self.out_suffix
@@ -189,7 +190,7 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.setup_extension('sphinx.builders.html')
app.add_builder(SingleFileHTMLBuilder)
diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py
index 441b598..8d5a1aa 100644
--- a/sphinx/builders/texinfo.py
+++ b/sphinx/builders/texinfo.py
@@ -17,7 +17,7 @@ from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.errors import NoUri
from sphinx.locale import _, __
from sphinx.util import logging
-from sphinx.util.console import darkgreen # type: ignore[attr-defined]
+from sphinx.util.console import darkgreen
from sphinx.util.display import progress_message, status_iterator
from sphinx.util.docutils import new_document
from sphinx.util.fileutil import copy_asset_file
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.config import Config
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
template_dir = os.path.join(package_dir, 'templates', 'texinfo')
@@ -41,6 +42,7 @@ class TexinfoBuilder(Builder):
"""
Builds Texinfo output to create Info documentation.
"""
+
name = 'texinfo'
format = 'texinfo'
epilog = __('The Texinfo files are in %(outdir)s.')
@@ -133,7 +135,7 @@ class TexinfoBuilder(Builder):
def assemble_doctree(
self, indexfile: str, toctree_only: bool, appendices: list[str],
) -> nodes.document:
- self.docnames = set([indexfile] + appendices)
+ self.docnames = {indexfile, *appendices}
logger.info(darkgreen(indexfile) + " ", nonl=True)
tree = self.env.get_doctree(indexfile)
tree['docname'] = indexfile
@@ -165,9 +167,11 @@ class TexinfoBuilder(Builder):
newnodes: list[Node] = [nodes.emphasis(sectname, sectname)]
for subdir, title in self.titles:
if docname.startswith(subdir):
- newnodes.append(nodes.Text(_(' (in ')))
- newnodes.append(nodes.emphasis(title, title))
- newnodes.append(nodes.Text(')'))
+ newnodes.extend((
+ nodes.Text(_(' (in ')),
+ nodes.emphasis(title, title),
+ nodes.Text(')'),
+ ))
break
else:
pass
@@ -205,22 +209,22 @@ class TexinfoBuilder(Builder):
def default_texinfo_documents(
config: Config,
) -> list[tuple[str, str, str, str, str, str, str]]:
- """ Better default texinfo_documents settings. """
+ """Better default texinfo_documents settings."""
filename = make_filename_from_project(config.project)
return [(config.root_doc, filename, config.project, config.author, filename,
'One line description of project', 'Miscellaneous')]
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(TexinfoBuilder)
- app.add_config_value('texinfo_documents', default_texinfo_documents, False)
- app.add_config_value('texinfo_appendices', [], False)
- app.add_config_value('texinfo_elements', {}, False)
- app.add_config_value('texinfo_domain_indices', True, False, [list])
- app.add_config_value('texinfo_show_urls', 'footnote', False)
- app.add_config_value('texinfo_no_detailmenu', False, False)
- app.add_config_value('texinfo_cross_references', True, False)
+ app.add_config_value('texinfo_documents', default_texinfo_documents, '')
+ app.add_config_value('texinfo_appendices', [], '')
+ app.add_config_value('texinfo_elements', {}, '')
+ app.add_config_value('texinfo_domain_indices', True, '', list)
+ app.add_config_value('texinfo_show_urls', 'footnote', '')
+ app.add_config_value('texinfo_no_detailmenu', False, '')
+ app.add_config_value('texinfo_cross_references', True, '')
return {
'version': 'builtin',
diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py
index 43a8d1f..483e7fa 100644
--- a/sphinx/builders/text.py
+++ b/sphinx/builders/text.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from os import path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils.io import StringOutput
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -79,7 +80,7 @@ class TextBuilder(Builder):
pass
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(TextBuilder)
app.add_config_value('text_sectionchars', '*=-~"+`', 'env')
diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py
index 5b88531..1f2c105 100644
--- a/sphinx/builders/xml.py
+++ b/sphinx/builders/xml.py
@@ -3,7 +3,7 @@
from __future__ import annotations
from os import path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils import nodes
from docutils.io import StringOutput
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
from docutils.nodes import Node
from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
logger = logging.getLogger(__name__)
@@ -29,6 +30,7 @@ class XMLBuilder(Builder):
"""
Builds Docutils-native XML.
"""
+
name = 'xml'
format = 'xml'
epilog = __('The XML files are in %(outdir)s.')
@@ -101,6 +103,7 @@ class PseudoXMLBuilder(XMLBuilder):
"""
Builds pseudo-XML for display purposes.
"""
+
name = 'pseudoxml'
format = 'pseudoxml'
epilog = __('The pseudo-XML files are in %(outdir)s.')
@@ -110,7 +113,7 @@ class PseudoXMLBuilder(XMLBuilder):
_writer_class = PseudoXMLWriter
-def setup(app: Sphinx) -> dict[str, Any]:
+def setup(app: Sphinx) -> ExtensionMetadata:
app.add_builder(XMLBuilder)
app.add_builder(PseudoXMLBuilder)