Skip to content

Commit

Permalink
Mark Builder.write() as final (#12767)
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner authored Oct 10, 2024
1 parent 705d5dd commit d135d2e
Show file tree
Hide file tree
Showing 18 changed files with 74 additions and 60 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions doc/_static/diagrams/sphinx_build_flow.dot
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions doc/_static/diagrams/sphinx_core_events_flow.dot
Original file line number Diff line number Diff line change
Expand Up @@ -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" [
Expand Down Expand Up @@ -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"};
}
9 changes: 6 additions & 3 deletions doc/extdev/builderapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 19 additions & 8 deletions sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
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

Expand Down Expand Up @@ -664,6 +664,7 @@ def write_doctree(
if _cache:
self.env._write_doc_doctree_cache[docname] = doctree

@final
def write(
self,
build_docnames: Iterable[str] | None,
Expand All @@ -685,11 +686,12 @@ def write(
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()))
Expand All @@ -700,12 +702,21 @@ def write(
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 (
Expand Down Expand Up @@ -769,9 +780,9 @@ def on_chunk_done(args: list[tuple[str, nodes.document]], result: None) -> None:
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"""
Expand Down
6 changes: 4 additions & 2 deletions sphinx/builders/changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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
Expand All @@ -16,6 +16,8 @@
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

Expand Down Expand Up @@ -46,7 +48,7 @@ def get_outdated_docs(self) -> str:
'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]]] = {}
Expand Down
3 changes: 0 additions & 3 deletions sphinx/builders/dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ def get_outdated_docs(self) -> set[str]:
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

Expand Down
4 changes: 3 additions & 1 deletion sphinx/builders/epub3.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
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

Expand Down Expand Up @@ -120,7 +122,7 @@ def content_metadata(self) -> dict[str, Any]:
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
Expand Down
3 changes: 0 additions & 3 deletions sphinx/builders/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,6 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str:
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

Expand Down
8 changes: 4 additions & 4 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
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
Expand Down Expand Up @@ -420,7 +420,7 @@ def render_partial(self, node: Node | None) -> dict[str, str]:
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:
Expand Down Expand Up @@ -965,9 +965,9 @@ def post_process_images(self, doctree: Node) -> None:
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:
Expand Down
12 changes: 6 additions & 6 deletions sphinx/builders/latex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
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

Expand Down Expand Up @@ -291,13 +291,17 @@ def write_stylesheet(self) -> None:
)
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)
Expand All @@ -309,10 +313,6 @@ def write(self, *ignored: Any) -> None:
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)
Expand Down
4 changes: 3 additions & 1 deletion sphinx/builders/manpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
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
Expand Down Expand Up @@ -55,7 +57,7 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str:
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)
Expand Down
9 changes: 4 additions & 5 deletions sphinx/builders/singlehtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
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
Expand Down Expand Up @@ -160,11 +162,8 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A
'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()
Expand Down
8 changes: 3 additions & 5 deletions sphinx/builders/texinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
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

Expand Down Expand Up @@ -71,7 +71,7 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
# 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(
Expand All @@ -98,9 +98,7 @@ def init_document_data(self) -> None:
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'
Expand Down
4 changes: 2 additions & 2 deletions sphinx/builders/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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

Expand Down Expand Up @@ -64,7 +64,7 @@ def get_outdated_docs(self) -> Iterator[str]:
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:
Expand Down
4 changes: 2 additions & 2 deletions sphinx/builders/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
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
Expand Down Expand Up @@ -68,7 +68,7 @@ def get_outdated_docs(self) -> Iterator[str]:
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:
Expand Down
Loading

0 comments on commit d135d2e

Please sign in to comment.