Mark `Builder.write()` as final (#12767)

This commit is contained in:
Adam Turner 2024-10-10 15:59:12 +01:00 committed by GitHub
parent 705d5ddd9f
commit d135d2eba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 74 additions and 60 deletions

View File

@ -149,6 +149,12 @@ Bugs fixed
* #12995: Significantly improve performance when building the search index
for Chinese languages.
Patch by Adam Turner.
* #12767: :py:meth:`.Builder.write` is typed as ``final``, meaning that the
:event:`write-started` event may be relied upon by extensions.
A new :py:meth:`.Builder.write_documents` method has been added to
control how documents are written.
This is intended for builders that do not output a file for each document.
Patch by Adam Turner.
Testing

View File

@ -27,10 +27,6 @@ digraph build {
"Builder.build_update":p1 -> "Builder.build";
"Builder.build" -> "Builder.read";
"Builder.write" [
shape=record
label = "<p1> Builder.write | Builder._write_serial | Builder._write_parallel"
];
"Builder.build" -> "Builder.write";
"Builder.build" -> "Builder.finish";
@ -39,8 +35,13 @@ digraph build {
"Builder.write":p1 -> "Builder.prepare_writing";
"Builder.write":p1 -> "Builder.copy_assets";
"Builder.write":p1 -> "Builder.write_doc";
"Builder.write_documents" [
shape=record
label = "<p1> Builder.write_documents | Builder._write_serial | Builder._write_parallel"
];
"Builder.write":p1 -> "Builder.write_documents";
"Builder.write_documents":p1 -> "Builder.write_doc";
"Builder.write_doc" -> "Builder.get_relative_uri";
"Builder.get_relative_uri" -> "Builder.get_target_uri";

View File

@ -83,9 +83,9 @@ digraph events {
// during write phase
"write-started"[style=filled fillcolor="#D5FFFF" color=blue penwidth=2];
"Builder.build":write -> "write-started";
"Builder.write" [label = "Builder.write()"]
"Builder.build":write -> "Builder.write";
"Builder.write" -> "write-started";
write_each_doc [shape="ellipse", label="for updated"];
"Builder.write" -> write_each_doc;
"ReferenceResolver" [
@ -120,6 +120,6 @@ digraph events {
{rank=same; "env-get-outdated" "env-before-read-docs" "env-get-updated"};
{rank=same; "env-purge-doc" "source-read" "doctree-read", "merge_each_process"};
{rank=same; "env-updated" "env-check-consistency"};
{rank=same; "env-merge-info" "write-started" "Builder.write"};
{rank=same; "env-merge-info" "Builder.write"};
{rank=max; "build-finished"};
}

View File

@ -39,20 +39,23 @@ Builder API
.. automethod:: read
.. automethod:: read_doc
.. automethod:: write_doctree
.. automethod:: write
.. rubric:: Overridable Methods
.. rubric:: Abstract Methods
These must be implemented in builder sub-classes:
.. automethod:: get_outdated_docs
.. automethod:: prepare_writing
.. automethod:: write_doc
.. automethod:: get_target_uri
.. rubric:: Overridable Methods
These methods can be overridden in builder sub-classes:
.. automethod:: init
.. automethod:: write
.. automethod:: write_documents
.. automethod:: prepare_writing
.. automethod:: copy_assets
.. automethod:: get_relative_uri
.. automethod:: finish

View File

@ -41,7 +41,7 @@ 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
from collections.abc import Iterable, Sequence, Set
from docutils.nodes import Node
@ -664,6 +664,7 @@ class Builder:
if _cache:
self.env._write_doc_doctree_cache[docname] = doctree
@final
def write(
self,
build_docnames: Iterable[str] | None,
@ -685,11 +686,12 @@ class Builder:
logger.debug(__('docnames to write: %s'), ', '.join(sorted(docnames)))
# add all toctree-containing files that may have changed
for docname in list(docnames):
extra = {self.config.root_doc}
for docname in docnames:
for tocdocname in self.env.files_to_rebuild.get(docname, set()):
if tocdocname in self.env.found_docs:
docnames.add(tocdocname)
docnames.add(self.config.root_doc)
extra.add(tocdocname)
docnames |= extra
# sort to ensure deterministic toctree generation
self.env.toctree_includes = dict(sorted(self.env.toctree_includes.items()))
@ -700,12 +702,21 @@ class Builder:
with progress_message(__('copying assets'), nonl=False):
self.copy_assets()
self.write_documents(docnames)
def write_documents(self, docnames: Set[str]) -> None:
"""Write all documents in *docnames*.
This method can be overridden if a builder does not create
output files for each document.
"""
sorted_docnames = sorted(docnames)
if self.parallel_ok:
# number of subprocesses is parallel-1 because the main process
# is busy loading doctrees and doing write_doc_serialized()
self._write_parallel(sorted(docnames), nproc=self.app.parallel - 1)
self._write_parallel(sorted_docnames, nproc=self.app.parallel - 1)
else:
self._write_serial(sorted(docnames))
self._write_serial(sorted_docnames)
def _write_serial(self, docnames: Sequence[str]) -> None:
with (
@ -769,9 +780,9 @@ class Builder:
tasks.join()
logger.info('')
def prepare_writing(self, docnames: set[str]) -> None:
def prepare_writing(self, docnames: Set[str]) -> None:
"""A place where you can add logic before :meth:`write_doc` is run"""
raise NotImplementedError
pass
def copy_assets(self) -> None:
"""Where assets (images, static files, etc) are copied before writing"""

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import html
from os import path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from sphinx import package_dir
from sphinx.builders import Builder
@ -16,6 +16,8 @@ from sphinx.util.fileutil import copy_asset_file
from sphinx.util.osutil import ensuredir, os_path
if TYPE_CHECKING:
from collections.abc import Set
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@ -46,7 +48,7 @@ class ChangesBuilder(Builder):
'versionremoved': 'removed',
}
def write(self, *ignored: Any) -> None:
def write_documents(self, _docnames: Set[str]) -> None:
version = self.config.version
domain = self.env.domains.changeset_domain
libchanges: dict[str, list[tuple[str, str, int]]] = {}

View File

@ -29,9 +29,6 @@ class DummyBuilder(Builder):
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
return ''
def prepare_writing(self, docnames: set[str]) -> None:
pass
def write_doc(self, docname: str, doctree: nodes.document) -> None:
pass

View File

@ -21,6 +21,8 @@ from sphinx.util.fileutil import copy_asset_file
from sphinx.util.osutil import make_filename
if TYPE_CHECKING:
from collections.abc import Set
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@ -120,7 +122,7 @@ class Epub3Builder(_epub_base.EpubBuilder):
metadata['epub_version'] = self.config.epub_version
return metadata
def prepare_writing(self, docnames: set[str]) -> None:
def prepare_writing(self, docnames: Set[str]) -> None:
super().prepare_writing(docnames)
writing_mode = self.config.epub_writing_mode

View File

@ -158,9 +158,6 @@ class I18nBuilder(Builder):
def get_outdated_docs(self) -> set[str]:
return self.env.found_docs
def prepare_writing(self, docnames: set[str]) -> None:
return
def compile_catalogs(self, catalogs: set[CatalogInfo], message: str) -> None:
return

View File

@ -64,7 +64,7 @@ from sphinx.writers.html import HTMLWriter
from sphinx.writers.html5 import HTML5Translator
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator
from collections.abc import Iterator, Set
from typing import TypeAlias
from docutils.nodes import Node
@ -420,7 +420,7 @@ class StandaloneHTMLBuilder(Builder):
self._publisher.publish()
return self._publisher.writer.parts
def prepare_writing(self, docnames: set[str]) -> None:
def prepare_writing(self, docnames: Set[str]) -> None:
# create the search indexer
self.indexer = None
if self.search:
@ -965,9 +965,9 @@ class StandaloneHTMLBuilder(Builder):
node.replace_self(reference)
reference.append(node)
def load_indexer(self, docnames: Iterable[str]) -> None:
def load_indexer(self, docnames: Set[str]) -> None:
assert self.indexer is not None
keep = set(self.env.all_docs) - set(docnames)
keep = set(self.env.all_docs).difference(docnames)
try:
searchindexfn = path.join(self.outdir, self.searchindex_filename)
if self.indexer_dumps_unicode:

View File

@ -38,7 +38,7 @@ from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter
from docutils import nodes # isort:skip
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterable, Set
from docutils.nodes import Node
@ -291,13 +291,17 @@ class LaTeXBuilder(Builder):
)
f.write(highlighter.get_stylesheet())
def prepare_writing(self, docnames: Set[str]) -> None:
self.init_document_data()
self.write_stylesheet()
def copy_assets(self) -> None:
self.copy_support_files()
if self.config.latex_additional_files:
self.copy_latex_additional_files()
def write(self, *ignored: Any) -> None:
def write_documents(self, _docnames: Set[str]) -> None:
docwriter = LaTeXWriter(self)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
@ -309,10 +313,6 @@ class LaTeXBuilder(Builder):
read_config_files=True,
).get_default_values()
self.init_document_data()
self.write_stylesheet()
self.copy_assets()
for entry in self.document_data:
docname, targetname, title, author, themename = entry[:5]
theme = self.themes.get(themename)

View File

@ -20,6 +20,8 @@ from sphinx.util.osutil import ensuredir, make_filename_from_project
from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
if TYPE_CHECKING:
from collections.abc import Set
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.util.typing import ExtensionMetadata
@ -55,7 +57,7 @@ class ManualPageBuilder(Builder):
return ''
@progress_message(__('writing'))
def write(self, *ignored: Any) -> None:
def write_documents(self, _docnames: Set[str]) -> None:
docwriter = ManualPageWriter(self)
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)

View File

@ -16,6 +16,8 @@ from sphinx.util.display import progress_message
from sphinx.util.nodes import inline_all_toctrees
if TYPE_CHECKING:
from collections.abc import Set
from docutils.nodes import Node
from sphinx.application import Sphinx
@ -160,11 +162,8 @@ class SingleFileHTMLBuilder(StandaloneHTMLBuilder):
'display_toc': display_toc,
}
def write(self, *ignored: Any) -> None:
docnames = self.env.all_docs
with progress_message(__('preparing documents')):
self.prepare_writing(docnames) # type: ignore[arg-type]
def write_documents(self, _docnames: Set[str]) -> None:
self.prepare_writing(self.env.all_docs.keys())
with progress_message(__('assembling single document'), nonl=False):
doctree = self.assemble_doctree()

View File

@ -25,7 +25,7 @@ from sphinx.util.osutil import SEP, copyfile, ensuredir, make_filename_from_proj
from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterable, Set
from docutils.nodes import Node
@ -71,7 +71,7 @@ class TexinfoBuilder(Builder):
# ignore source path
return self.get_target_uri(to, typ)
def init_document_data(self) -> None:
def prepare_writing(self, _docnames: Set[str]) -> None:
preliminary_document_data = [list(x) for x in self.config.texinfo_documents]
if not preliminary_document_data:
logger.warning(
@ -98,9 +98,7 @@ class TexinfoBuilder(Builder):
docname = docname.removesuffix(SEP + 'index')
self.titles.append((docname, entry[2]))
def write(self, *ignored: Any) -> None:
self.init_document_data()
self.copy_assets()
def write_documents(self, _docnames: Set[str]) -> None:
for entry in self.document_data:
docname, targetname, title, author = entry[:4]
targetname += '.texi'

View File

@ -18,7 +18,7 @@ from sphinx.util.osutil import (
from sphinx.writers.text import TextTranslator, TextWriter
if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Set
from docutils import nodes
@ -64,7 +64,7 @@ class TextBuilder(Builder):
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
return ''
def prepare_writing(self, docnames: set[str]) -> None:
def prepare_writing(self, docnames: Set[str]) -> None:
self.writer = TextWriter(self)
def write_doc(self, docname: str, doctree: nodes.document) -> None:

View File

@ -20,7 +20,7 @@ from sphinx.util.osutil import (
from sphinx.writers.xml import PseudoXMLWriter, XMLWriter
if TYPE_CHECKING:
from collections.abc import Iterator
from collections.abc import Iterator, Set
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@ -68,7 +68,7 @@ class XMLBuilder(Builder):
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
return docname
def prepare_writing(self, docnames: set[str]) -> None:
def prepare_writing(self, docnames: Set[str]) -> None:
self.writer = self._writer_class(self)
def write_doc(self, docname: str, doctree: nodes.document) -> None:

View File

@ -192,7 +192,7 @@ class CoverageBuilder(Builder):
def get_outdated_docs(self) -> str:
return 'coverage overview'
def write(self, *ignored: Any) -> None:
def write_documents(self, _docnames: Set[str]) -> None:
self.py_undoc: dict[str, dict[str, Any]] = {}
self.py_undocumented: dict[str, Set[str]] = {}
self.py_documented: dict[str, Set[str]] = {}

View File

@ -27,7 +27,7 @@ from sphinx.util.docutils import SphinxDirective
from sphinx.util.osutil import relpath
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence
from collections.abc import Callable, Set
from docutils.nodes import Element, Node, TextElement
@ -355,13 +355,9 @@ Doctest summary
if self.total_failures or self.setup_failures or self.cleanup_failures:
self.app.statuscode = 1
def write(self, build_docnames: Iterable[str] | None, updated_docnames: Sequence[str],
method: str = 'update') -> None:
if build_docnames is None:
build_docnames = sorted(self.env.all_docs)
def write_documents(self, docnames: Set[str]) -> None:
logger.info(bold('running tests...'))
for docname in build_docnames:
for docname in sorted(docnames):
# no need to resolve the doctree
doctree = self.env.get_doctree(docname)
self.test_doc(docname, doctree)