Skip to content

Commit d135d2e

Browse files
authored
Mark Builder.write() as final (#12767)
1 parent 705d5dd commit d135d2e

18 files changed

+74
-60
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ Bugs fixed
149149
* #12995: Significantly improve performance when building the search index
150150
for Chinese languages.
151151
Patch by Adam Turner.
152+
* #12767: :py:meth:`.Builder.write` is typed as ``final``, meaning that the
153+
:event:`write-started` event may be relied upon by extensions.
154+
A new :py:meth:`.Builder.write_documents` method has been added to
155+
control how documents are written.
156+
This is intended for builders that do not output a file for each document.
157+
Patch by Adam Turner.
152158

153159

154160
Testing

doc/_static/diagrams/sphinx_build_flow.dot

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ digraph build {
2727
"Builder.build_update":p1 -> "Builder.build";
2828

2929
"Builder.build" -> "Builder.read";
30-
"Builder.write" [
31-
shape=record
32-
label = "<p1> Builder.write | Builder._write_serial | Builder._write_parallel"
33-
];
3430
"Builder.build" -> "Builder.write";
3531
"Builder.build" -> "Builder.finish";
3632

@@ -39,8 +35,13 @@ digraph build {
3935

4036
"Builder.write":p1 -> "Builder.prepare_writing";
4137
"Builder.write":p1 -> "Builder.copy_assets";
42-
"Builder.write":p1 -> "Builder.write_doc";
38+
"Builder.write_documents" [
39+
shape=record
40+
label = "<p1> Builder.write_documents | Builder._write_serial | Builder._write_parallel"
41+
];
42+
"Builder.write":p1 -> "Builder.write_documents";
4343

44+
"Builder.write_documents":p1 -> "Builder.write_doc";
4445
"Builder.write_doc" -> "Builder.get_relative_uri";
4546

4647
"Builder.get_relative_uri" -> "Builder.get_target_uri";

doc/_static/diagrams/sphinx_core_events_flow.dot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ digraph events {
8383

8484
// during write phase
8585
"write-started"[style=filled fillcolor="#D5FFFF" color=blue penwidth=2];
86-
"Builder.build":write -> "write-started";
8786
"Builder.write" [label = "Builder.write()"]
8887
"Builder.build":write -> "Builder.write";
88+
"Builder.write" -> "write-started";
8989
write_each_doc [shape="ellipse", label="for updated"];
9090
"Builder.write" -> write_each_doc;
9191
"ReferenceResolver" [
@@ -120,6 +120,6 @@ digraph events {
120120
{rank=same; "env-get-outdated" "env-before-read-docs" "env-get-updated"};
121121
{rank=same; "env-purge-doc" "source-read" "doctree-read", "merge_each_process"};
122122
{rank=same; "env-updated" "env-check-consistency"};
123-
{rank=same; "env-merge-info" "write-started" "Builder.write"};
123+
{rank=same; "env-merge-info" "Builder.write"};
124124
{rank=max; "build-finished"};
125125
}

doc/extdev/builderapi.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,23 @@ Builder API
3939
.. automethod:: read
4040
.. automethod:: read_doc
4141
.. automethod:: write_doctree
42+
.. automethod:: write
4243

43-
.. rubric:: Overridable Methods
44+
.. rubric:: Abstract Methods
4445

4546
These must be implemented in builder sub-classes:
4647

4748
.. automethod:: get_outdated_docs
48-
.. automethod:: prepare_writing
4949
.. automethod:: write_doc
5050
.. automethod:: get_target_uri
5151

52+
.. rubric:: Overridable Methods
53+
5254
These methods can be overridden in builder sub-classes:
5355

5456
.. automethod:: init
55-
.. automethod:: write
57+
.. automethod:: write_documents
58+
.. automethod:: prepare_writing
5659
.. automethod:: copy_assets
5760
.. automethod:: get_relative_uri
5861
.. automethod:: finish

sphinx/builders/__init__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from sphinx import roles # NoQA: F401 isort:skip
4242

4343
if TYPE_CHECKING:
44-
from collections.abc import Iterable, Sequence
44+
from collections.abc import Iterable, Sequence, Set
4545

4646
from docutils.nodes import Node
4747

@@ -664,6 +664,7 @@ def write_doctree(
664664
if _cache:
665665
self.env._write_doc_doctree_cache[docname] = doctree
666666

667+
@final
667668
def write(
668669
self,
669670
build_docnames: Iterable[str] | None,
@@ -685,11 +686,12 @@ def write(
685686
logger.debug(__('docnames to write: %s'), ', '.join(sorted(docnames)))
686687

687688
# add all toctree-containing files that may have changed
688-
for docname in list(docnames):
689+
extra = {self.config.root_doc}
690+
for docname in docnames:
689691
for tocdocname in self.env.files_to_rebuild.get(docname, set()):
690692
if tocdocname in self.env.found_docs:
691-
docnames.add(tocdocname)
692-
docnames.add(self.config.root_doc)
693+
extra.add(tocdocname)
694+
docnames |= extra
693695

694696
# sort to ensure deterministic toctree generation
695697
self.env.toctree_includes = dict(sorted(self.env.toctree_includes.items()))
@@ -700,12 +702,21 @@ def write(
700702
with progress_message(__('copying assets'), nonl=False):
701703
self.copy_assets()
702704

705+
self.write_documents(docnames)
706+
707+
def write_documents(self, docnames: Set[str]) -> None:
708+
"""Write all documents in *docnames*.
709+
710+
This method can be overridden if a builder does not create
711+
output files for each document.
712+
"""
713+
sorted_docnames = sorted(docnames)
703714
if self.parallel_ok:
704715
# number of subprocesses is parallel-1 because the main process
705716
# is busy loading doctrees and doing write_doc_serialized()
706-
self._write_parallel(sorted(docnames), nproc=self.app.parallel - 1)
717+
self._write_parallel(sorted_docnames, nproc=self.app.parallel - 1)
707718
else:
708-
self._write_serial(sorted(docnames))
719+
self._write_serial(sorted_docnames)
709720

710721
def _write_serial(self, docnames: Sequence[str]) -> None:
711722
with (
@@ -769,9 +780,9 @@ def on_chunk_done(args: list[tuple[str, nodes.document]], result: None) -> None:
769780
tasks.join()
770781
logger.info('')
771782

772-
def prepare_writing(self, docnames: set[str]) -> None:
783+
def prepare_writing(self, docnames: Set[str]) -> None:
773784
"""A place where you can add logic before :meth:`write_doc` is run"""
774-
raise NotImplementedError
785+
pass
775786

776787
def copy_assets(self) -> None:
777788
"""Where assets (images, static files, etc) are copied before writing"""

sphinx/builders/changes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import html
66
from os import path
7-
from typing import TYPE_CHECKING, Any
7+
from typing import TYPE_CHECKING
88

99
from sphinx import package_dir
1010
from sphinx.builders import Builder
@@ -16,6 +16,8 @@
1616
from sphinx.util.osutil import ensuredir, os_path
1717

1818
if TYPE_CHECKING:
19+
from collections.abc import Set
20+
1921
from sphinx.application import Sphinx
2022
from sphinx.util.typing import ExtensionMetadata
2123

@@ -46,7 +48,7 @@ def get_outdated_docs(self) -> str:
4648
'versionremoved': 'removed',
4749
}
4850

49-
def write(self, *ignored: Any) -> None:
51+
def write_documents(self, _docnames: Set[str]) -> None:
5052
version = self.config.version
5153
domain = self.env.domains.changeset_domain
5254
libchanges: dict[str, list[tuple[str, str, int]]] = {}

sphinx/builders/dummy.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ def get_outdated_docs(self) -> set[str]:
2929
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
3030
return ''
3131

32-
def prepare_writing(self, docnames: set[str]) -> None:
33-
pass
34-
3532
def write_doc(self, docname: str, doctree: nodes.document) -> None:
3633
pass
3734

sphinx/builders/epub3.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from sphinx.util.osutil import make_filename
2222

2323
if TYPE_CHECKING:
24+
from collections.abc import Set
25+
2426
from sphinx.application import Sphinx
2527
from sphinx.util.typing import ExtensionMetadata
2628

@@ -120,7 +122,7 @@ def content_metadata(self) -> dict[str, Any]:
120122
metadata['epub_version'] = self.config.epub_version
121123
return metadata
122124

123-
def prepare_writing(self, docnames: set[str]) -> None:
125+
def prepare_writing(self, docnames: Set[str]) -> None:
124126
super().prepare_writing(docnames)
125127

126128
writing_mode = self.config.epub_writing_mode

sphinx/builders/gettext.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,6 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str:
158158
def get_outdated_docs(self) -> set[str]:
159159
return self.env.found_docs
160160

161-
def prepare_writing(self, docnames: set[str]) -> None:
162-
return
163-
164161
def compile_catalogs(self, catalogs: set[CatalogInfo], message: str) -> None:
165162
return
166163

sphinx/builders/html/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
from sphinx.writers.html5 import HTML5Translator
6565

6666
if TYPE_CHECKING:
67-
from collections.abc import Iterable, Iterator
67+
from collections.abc import Iterator, Set
6868
from typing import TypeAlias
6969

7070
from docutils.nodes import Node
@@ -420,7 +420,7 @@ def render_partial(self, node: Node | None) -> dict[str, str]:
420420
self._publisher.publish()
421421
return self._publisher.writer.parts
422422

423-
def prepare_writing(self, docnames: set[str]) -> None:
423+
def prepare_writing(self, docnames: Set[str]) -> None:
424424
# create the search indexer
425425
self.indexer = None
426426
if self.search:
@@ -965,9 +965,9 @@ def post_process_images(self, doctree: Node) -> None:
965965
node.replace_self(reference)
966966
reference.append(node)
967967

968-
def load_indexer(self, docnames: Iterable[str]) -> None:
968+
def load_indexer(self, docnames: Set[str]) -> None:
969969
assert self.indexer is not None
970-
keep = set(self.env.all_docs) - set(docnames)
970+
keep = set(self.env.all_docs).difference(docnames)
971971
try:
972972
searchindexfn = path.join(self.outdir, self.searchindex_filename)
973973
if self.indexer_dumps_unicode:

sphinx/builders/latex/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from docutils import nodes # isort:skip
3939

4040
if TYPE_CHECKING:
41-
from collections.abc import Iterable
41+
from collections.abc import Iterable, Set
4242

4343
from docutils.nodes import Node
4444

@@ -291,13 +291,17 @@ def write_stylesheet(self) -> None:
291291
)
292292
f.write(highlighter.get_stylesheet())
293293

294+
def prepare_writing(self, docnames: Set[str]) -> None:
295+
self.init_document_data()
296+
self.write_stylesheet()
297+
294298
def copy_assets(self) -> None:
295299
self.copy_support_files()
296300

297301
if self.config.latex_additional_files:
298302
self.copy_latex_additional_files()
299303

300-
def write(self, *ignored: Any) -> None:
304+
def write_documents(self, _docnames: Set[str]) -> None:
301305
docwriter = LaTeXWriter(self)
302306
with warnings.catch_warnings():
303307
warnings.filterwarnings('ignore', category=DeprecationWarning)
@@ -309,10 +313,6 @@ def write(self, *ignored: Any) -> None:
309313
read_config_files=True,
310314
).get_default_values()
311315

312-
self.init_document_data()
313-
self.write_stylesheet()
314-
self.copy_assets()
315-
316316
for entry in self.document_data:
317317
docname, targetname, title, author, themename = entry[:5]
318318
theme = self.themes.get(themename)

sphinx/builders/manpage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
2121

2222
if TYPE_CHECKING:
23+
from collections.abc import Set
24+
2325
from sphinx.application import Sphinx
2426
from sphinx.config import Config
2527
from sphinx.util.typing import ExtensionMetadata
@@ -55,7 +57,7 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str:
5557
return ''
5658

5759
@progress_message(__('writing'))
58-
def write(self, *ignored: Any) -> None:
60+
def write_documents(self, _docnames: Set[str]) -> None:
5961
docwriter = ManualPageWriter(self)
6062
with warnings.catch_warnings():
6163
warnings.filterwarnings('ignore', category=DeprecationWarning)

sphinx/builders/singlehtml.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from sphinx.util.nodes import inline_all_toctrees
1717

1818
if TYPE_CHECKING:
19+
from collections.abc import Set
20+
1921
from docutils.nodes import Node
2022

2123
from sphinx.application import Sphinx
@@ -160,11 +162,8 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A
160162
'display_toc': display_toc,
161163
}
162164

163-
def write(self, *ignored: Any) -> None:
164-
docnames = self.env.all_docs
165-
166-
with progress_message(__('preparing documents')):
167-
self.prepare_writing(docnames) # type: ignore[arg-type]
165+
def write_documents(self, _docnames: Set[str]) -> None:
166+
self.prepare_writing(self.env.all_docs.keys())
168167

169168
with progress_message(__('assembling single document'), nonl=False):
170169
doctree = self.assemble_doctree()

sphinx/builders/texinfo.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from sphinx.writers.texinfo import TexinfoTranslator, TexinfoWriter
2626

2727
if TYPE_CHECKING:
28-
from collections.abc import Iterable
28+
from collections.abc import Iterable, Set
2929

3030
from docutils.nodes import Node
3131

@@ -71,7 +71,7 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str:
7171
# ignore source path
7272
return self.get_target_uri(to, typ)
7373

74-
def init_document_data(self) -> None:
74+
def prepare_writing(self, _docnames: Set[str]) -> None:
7575
preliminary_document_data = [list(x) for x in self.config.texinfo_documents]
7676
if not preliminary_document_data:
7777
logger.warning(
@@ -98,9 +98,7 @@ def init_document_data(self) -> None:
9898
docname = docname.removesuffix(SEP + 'index')
9999
self.titles.append((docname, entry[2]))
100100

101-
def write(self, *ignored: Any) -> None:
102-
self.init_document_data()
103-
self.copy_assets()
101+
def write_documents(self, _docnames: Set[str]) -> None:
104102
for entry in self.document_data:
105103
docname, targetname, title, author = entry[:4]
106104
targetname += '.texi'

sphinx/builders/text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from sphinx.writers.text import TextTranslator, TextWriter
1919

2020
if TYPE_CHECKING:
21-
from collections.abc import Iterator
21+
from collections.abc import Iterator, Set
2222

2323
from docutils import nodes
2424

@@ -64,7 +64,7 @@ def get_outdated_docs(self) -> Iterator[str]:
6464
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
6565
return ''
6666

67-
def prepare_writing(self, docnames: set[str]) -> None:
67+
def prepare_writing(self, docnames: Set[str]) -> None:
6868
self.writer = TextWriter(self)
6969

7070
def write_doc(self, docname: str, doctree: nodes.document) -> None:

sphinx/builders/xml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from sphinx.writers.xml import PseudoXMLWriter, XMLWriter
2121

2222
if TYPE_CHECKING:
23-
from collections.abc import Iterator
23+
from collections.abc import Iterator, Set
2424

2525
from sphinx.application import Sphinx
2626
from sphinx.util.typing import ExtensionMetadata
@@ -68,7 +68,7 @@ def get_outdated_docs(self) -> Iterator[str]:
6868
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
6969
return docname
7070

71-
def prepare_writing(self, docnames: set[str]) -> None:
71+
def prepare_writing(self, docnames: Set[str]) -> None:
7272
self.writer = self._writer_class(self)
7373

7474
def write_doc(self, docname: str, doctree: nodes.document) -> None:

0 commit comments

Comments
 (0)