diff options
Diffstat (limited to 'sphinx/builders')
-rw-r--r-- | sphinx/builders/__init__.py | 11 | ||||
-rw-r--r-- | sphinx/builders/_epub_base.py | 26 | ||||
-rw-r--r-- | sphinx/builders/changes.py | 15 | ||||
-rw-r--r-- | sphinx/builders/dirhtml.py | 6 | ||||
-rw-r--r-- | sphinx/builders/dummy.py | 5 | ||||
-rw-r--r-- | sphinx/builders/epub3.py | 8 | ||||
-rw-r--r-- | sphinx/builders/gettext.py | 31 | ||||
-rw-r--r-- | sphinx/builders/html/__init__.py | 184 | ||||
-rw-r--r-- | sphinx/builders/html/_assets.py | 32 | ||||
-rw-r--r-- | sphinx/builders/html/transforms.py | 6 | ||||
-rw-r--r-- | sphinx/builders/latex/__init__.py | 73 | ||||
-rw-r--r-- | sphinx/builders/latex/nodes.py | 8 | ||||
-rw-r--r-- | sphinx/builders/latex/transforms.py | 25 | ||||
-rw-r--r-- | sphinx/builders/latex/util.py | 2 | ||||
-rw-r--r-- | sphinx/builders/linkcheck.py | 113 | ||||
-rw-r--r-- | sphinx/builders/manpage.py | 14 | ||||
-rw-r--r-- | sphinx/builders/singlehtml.py | 9 | ||||
-rw-r--r-- | sphinx/builders/texinfo.py | 32 | ||||
-rw-r--r-- | sphinx/builders/text.py | 5 | ||||
-rw-r--r-- | sphinx/builders/xml.py | 7 |
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) |