Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mark Builder.write() as final #12767

Merged
merged 11 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ Bugs fixed
and ensure deterministic resolution of global toctree in parallel builds
by choosing the lexicographically greatest parent document.
Patch by A. Rafey Khan
* #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" [
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
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"};
}
5 changes: 3 additions & 2 deletions doc/extdev/builderapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,21 @@ Builder API
.. automethod:: read
.. automethod:: read_doc
.. automethod:: write_doctree
.. automethod:: write

.. rubric:: Overridable Methods
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved

These must be implemented in builder sub-classes:

.. automethod:: get_outdated_docs
.. automethod:: prepare_writing
.. automethod:: write_doc
.. automethod:: get_target_uri

These methods can be overridden in builder sub-classes:
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved

.. 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
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved

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:
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
"""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:
AA-Turner marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 1 addition & 1 deletion sphinx/ext/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def init(self) -> None:
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]] = {}
Expand Down
Loading
Loading