Skip to content

Commit c384ab9

Browse files
authored
Use Docutils translators directly in the writing phase (#13683)
1 parent f1edefe commit c384ab9

File tree

13 files changed

+115
-129
lines changed

13 files changed

+115
-129
lines changed

sphinx/builders/html/__init__.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
from typing import TYPE_CHECKING
1616
from urllib.parse import quote
1717

18+
import docutils.parsers.rst
1819
import docutils.readers.doctree
1920
import docutils.utils
2021
import jinja2.exceptions
2122
from docutils import nodes
22-
from docutils.core import Publisher
23-
from docutils.io import DocTreeInput, StringOutput
2423

2524
from sphinx import __display_version__, package_dir
2625
from sphinx import version_info as sphinx_version
@@ -69,7 +68,6 @@
6968
from typing import Any, TypeAlias
7069

7170
from docutils.nodes import Node
72-
from docutils.readers import Reader
7371

7472
from sphinx.application import Sphinx
7573
from sphinx.config import Config
@@ -93,6 +91,10 @@
9391
bool,
9492
]
9593

94+
_READER_TRANSFORMS = docutils.readers.doctree.Reader().get_transforms()
95+
_PARSER_TRANSFORMS = docutils.parsers.rst.Parser().get_transforms()
96+
_WRITER_TRANSFORMS = HTMLWriter(None).get_transforms() # type: ignore[arg-type]
97+
9698

9799
def convert_locale_to_language_tag(locale: str | None) -> str | None:
98100
"""Convert a locale string to a language tag (ex. en_US -> en-US).
@@ -150,19 +152,13 @@ def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
150152
# JS files
151153
self._js_files: list[_JavaScript] = []
152154

153-
# Cached Publisher for writing doctrees to HTML
154-
reader: Reader[DocTreeInput] = docutils.readers.doctree.Reader(
155-
parser_name='restructuredtext'
156-
)
157-
pub = Publisher(
158-
reader=reader,
159-
parser=reader.parser,
160-
writer=HTMLWriter(self),
161-
source_class=DocTreeInput,
162-
destination=StringOutput(encoding='unicode'),
155+
# Cached settings for render_partial()
156+
self._settings = _get_settings(
157+
docutils.readers.doctree.Reader,
158+
docutils.parsers.rst.Parser,
159+
HTMLWriter,
160+
defaults={'output_encoding': 'unicode', 'traceback': True},
163161
)
164-
pub.get_settings(output_encoding='unicode', traceback=True)
165-
self._publisher = pub
166162

167163
def init(self) -> None:
168164
self.build_info = self.create_build_info()
@@ -428,10 +424,11 @@ def render_partial(self, node: Node | None) -> dict[str, str]:
428424
"""Utility: Render a lone doctree node."""
429425
if node is None:
430426
return {'fragment': ''}
431-
pub = self._publisher
432-
doc = docutils.utils.new_document('<partial node>', pub.settings)
427+
doc = docutils.utils.new_document('<partial node>', self._settings)
433428
doc.append(node)
434-
doc.transformer.populate_from_components((pub.reader, pub.parser, pub.writer))
429+
doc.transformer.add_transforms(_READER_TRANSFORMS)
430+
doc.transformer.add_transforms(_PARSER_TRANSFORMS)
431+
doc.transformer.add_transforms(_WRITER_TRANSFORMS)
435432
doc.transformer.apply_transforms()
436433
visitor: HTML5Translator = self.create_translator(doc, self) # type: ignore[assignment]
437434
doc.walkabout(visitor)
@@ -456,7 +453,6 @@ def prepare_writing(self, docnames: Set[str]) -> None:
456453
)
457454
self.load_indexer(docnames)
458455

459-
self.docwriter = HTMLWriter(self)
460456
self.docsettings = _get_settings(
461457
HTMLWriter, defaults=self.env.settings, read_config_files=True
462458
)
@@ -666,21 +662,20 @@ def copy_assets(self) -> None:
666662
self.finish_tasks.join()
667663

668664
def write_doc(self, docname: str, doctree: nodes.document) -> None:
669-
destination = StringOutput(encoding='utf-8')
670665
doctree.settings = self.docsettings
671666

672667
self.secnumbers = self.env.toc_secnumbers.get(docname, {})
673668
self.fignumbers = self.env.toc_fignumbers.get(docname, {})
674669
self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
675670
self.dlpath = relative_uri(self.get_target_uri(docname), '_downloads')
676671
self.current_docname = docname
677-
self.docwriter.write(doctree, destination)
678-
self.docwriter.assemble_parts()
679-
body = self.docwriter.parts['fragment']
680-
metatags = self.docwriter.clean_meta
672+
visitor: HTML5Translator = self.create_translator(doctree, self) # type: ignore[assignment]
673+
doctree.walkabout(visitor)
674+
body = ''.join(visitor.fragment)
675+
clean_meta = ''.join(visitor.meta[2:])
681676

682-
ctx = self.get_doc_context(docname, body, metatags)
683-
ctx['has_maths_elements'] = self.docwriter._has_maths_elements
677+
ctx = self.get_doc_context(docname, body, clean_meta)
678+
ctx['has_maths_elements'] = getattr(visitor, '_has_maths_elements', False)
684679
self.handle_page(docname, ctx, event_arg=doctree)
685680

686681
def write_doc_serialized(self, docname: str, doctree: nodes.document) -> None:

sphinx/builders/latex/__init__.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from sphinx.locale import _, __
2525
from sphinx.util import logging, texescape
2626
from sphinx.util.display import progress_message, status_iterator
27-
from sphinx.util.docutils import SphinxFileOutput, _get_settings, new_document
27+
from sphinx.util.docutils import _get_settings, new_document
2828
from sphinx.util.fileutil import copy_asset_file
2929
from sphinx.util.i18n import format_date
3030
from sphinx.util.nodes import inline_all_toctrees
@@ -297,7 +297,6 @@ def copy_assets(self) -> None:
297297
self.copy_latex_additional_files()
298298

299299
def write_documents(self, _docnames: Set[str]) -> None:
300-
docwriter = LaTeXWriter(self)
301300
docsettings = _get_settings(
302301
LaTeXWriter, defaults=self.env.settings, read_config_files=True
303302
)
@@ -308,11 +307,6 @@ def write_documents(self, _docnames: Set[str]) -> None:
308307
toctree_only = False
309308
if len(entry) > 5:
310309
toctree_only = entry[5]
311-
destination = SphinxFileOutput(
312-
destination_path=self.outdir / targetname,
313-
encoding='utf-8',
314-
overwrite_if_changed=True,
315-
)
316310
with progress_message(__('processing %s') % targetname, nonl=False):
317311
doctree = self.env.get_doctree(docname)
318312
toctree = next(doctree.findall(addnodes.toctree), None)
@@ -343,8 +337,16 @@ def write_documents(self, _docnames: Set[str]) -> None:
343337
docsettings._docclass = theme.name
344338

345339
doctree.settings = docsettings
346-
docwriter.theme = theme
347-
docwriter.write(doctree, destination)
340+
visitor: LaTeXTranslator = self.create_translator(doctree, self, theme) # type: ignore[assignment]
341+
doctree.walkabout(visitor)
342+
output = visitor.astext()
343+
destination_path = self.outdir / targetname
344+
# https://github.com/sphinx-doc/sphinx/issues/4362
345+
if (
346+
not destination_path.is_file()
347+
or destination_path.read_bytes() != output.encode()
348+
):
349+
destination_path.write_text(output, encoding='utf-8')
348350

349351
def get_contentsname(self, indexfile: str) -> str:
350352
tree = self.env.get_doctree(indexfile)

sphinx/builders/manpage.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
from typing import TYPE_CHECKING
66

7-
from docutils.io import FileOutput
8-
97
from sphinx import addnodes
108
from sphinx._cli.util.colour import darkgreen
119
from sphinx.builders import Builder
@@ -15,7 +13,11 @@
1513
from sphinx.util.docutils import _get_settings
1614
from sphinx.util.nodes import inline_all_toctrees
1715
from sphinx.util.osutil import ensuredir, make_filename_from_project
18-
from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
16+
from sphinx.writers.manpage import (
17+
ManualPageTranslator,
18+
ManualPageWriter,
19+
NestedInlineTransform,
20+
)
1921

2022
if TYPE_CHECKING:
2123
from collections.abc import Set
@@ -51,7 +53,6 @@ def get_target_uri(self, docname: str, typ: str | None = None) -> str:
5153

5254
@progress_message(__('writing'))
5355
def write_documents(self, _docnames: Set[str]) -> None:
54-
docwriter = ManualPageWriter(self)
5556
docsettings = _get_settings(
5657
ManualPageWriter, defaults=self.env.settings, read_config_files=True
5758
)
@@ -83,10 +84,6 @@ def write_documents(self, _docnames: Set[str]) -> None:
8384
targetname = f'{name}.{section}'
8485

8586
logger.info('%s { ', darkgreen(targetname))
86-
destination = FileOutput(
87-
destination_path=self.outdir / targetname,
88-
encoding='utf-8',
89-
)
9087

9188
tree = self.env.get_doctree(docname)
9289
docnames: set[str] = set()
@@ -100,7 +97,11 @@ def write_documents(self, _docnames: Set[str]) -> None:
10097
for pendingnode in largetree.findall(addnodes.pending_xref):
10198
pendingnode.replace_self(pendingnode.children)
10299

103-
docwriter.write(largetree, destination)
100+
transform = NestedInlineTransform(largetree)
101+
transform.apply()
102+
visitor: ManualPageTranslator = self.create_translator(largetree, self) # type: ignore[assignment]
103+
largetree.walkabout(visitor)
104+
(self.outdir / targetname).write_text(visitor.astext(), encoding='utf-8')
104105

105106
def finish(self) -> None:
106107
pass

sphinx/builders/texinfo.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import TYPE_CHECKING
77

88
from docutils import nodes
9-
from docutils.io import FileOutput
109

1110
from sphinx import addnodes, package_dir
1211
from sphinx._cli.util.colour import darkgreen
@@ -103,10 +102,6 @@ def write_documents(self, _docnames: Set[str]) -> None:
103102
toctree_only = False
104103
if len(entry) > 7:
105104
toctree_only = entry[7]
106-
destination = FileOutput(
107-
destination_path=self.outdir / targetname,
108-
encoding='utf-8',
109-
)
110105
with progress_message(__('processing %s') % targetname, nonl=False):
111106
appendices = self.config.texinfo_appendices or []
112107
doctree = self.assemble_doctree(
@@ -115,7 +110,6 @@ def write_documents(self, _docnames: Set[str]) -> None:
115110

116111
with progress_message(__('writing')):
117112
self.post_process_images(doctree)
118-
docwriter = TexinfoWriter(self)
119113
settings = _get_settings(
120114
TexinfoWriter, defaults=self.env.settings, read_config_files=True
121115
)
@@ -128,7 +122,10 @@ def write_documents(self, _docnames: Set[str]) -> None:
128122
settings.texinfo_dir_description = description or ''
129123
settings.docname = docname
130124
doctree.settings = settings
131-
docwriter.write(doctree, destination)
125+
visitor: TexinfoTranslator = self.create_translator(doctree, self) # type: ignore[assignment]
126+
doctree.walkabout(visitor)
127+
visitor.finish()
128+
(self.outdir / targetname).write_text(visitor.output, encoding='utf-8')
132129
self.copy_image_files(targetname[:-5])
133130

134131
def assemble_doctree(

sphinx/builders/text.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44

55
from typing import TYPE_CHECKING
66

7-
from docutils.io import StringOutput
8-
97
from sphinx.builders import Builder
108
from sphinx.locale import __
119
from sphinx.util import logging
1210
from sphinx.util.osutil import _last_modified_time
13-
from sphinx.writers.text import TextTranslator, TextWriter
11+
from sphinx.writers.text import TextTranslator
1412

1513
if TYPE_CHECKING:
16-
from collections.abc import Iterator, Set
14+
from collections.abc import Iterator
1715

1816
from docutils import nodes
1917

@@ -59,19 +57,16 @@ def get_outdated_docs(self) -> Iterator[str]:
5957
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
6058
return ''
6159

62-
def prepare_writing(self, docnames: Set[str]) -> None:
63-
self.writer = TextWriter(self)
64-
6560
def write_doc(self, docname: str, doctree: nodes.document) -> None:
6661
self.current_docname = docname
6762
self.secnumbers = self.env.toc_secnumbers.get(docname, {})
68-
destination = StringOutput(encoding='utf-8')
69-
self.writer.write(doctree, destination)
63+
visitor: TextTranslator = self.create_translator(doctree, self) # type: ignore[assignment]
64+
doctree.walkabout(visitor)
65+
output = visitor.body
7066
out_file_name = self.outdir / (docname + self.out_suffix)
7167
out_file_name.parent.mkdir(parents=True, exist_ok=True)
7268
try:
73-
with open(out_file_name, 'w', encoding='utf-8') as f:
74-
f.write(self.writer.output)
69+
out_file_name.write_text(output, encoding='utf-8')
7570
except OSError as err:
7671
logger.warning(__('error writing file %s: %s'), out_file_name, err)
7772

sphinx/builders/xml.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@
55
from typing import TYPE_CHECKING
66

77
from docutils import nodes
8-
from docutils.io import StringOutput
98
from docutils.writers.docutils_xml import XMLTranslator
109

1110
from sphinx.builders import Builder
1211
from sphinx.locale import __
1312
from sphinx.util import logging
1413
from sphinx.util.osutil import _last_modified_time
15-
from sphinx.writers.xml import PseudoXMLWriter, XMLWriter
1614

1715
if TYPE_CHECKING:
18-
from collections.abc import Iterator, Set
16+
from collections.abc import Iterator
1917

2018
from sphinx.application import Sphinx
2119
from sphinx.util.typing import ExtensionMetadata
@@ -33,8 +31,6 @@ class XMLBuilder(Builder):
3331
out_suffix = '.xml'
3432
allow_parallel = True
3533

36-
_writer_class: type[XMLWriter | PseudoXMLWriter] = XMLWriter
37-
writer: XMLWriter | PseudoXMLWriter
3834
default_translator_class = XMLTranslator
3935

4036
def init(self) -> None:
@@ -61,9 +57,6 @@ def get_outdated_docs(self) -> Iterator[str]:
6157
def get_target_uri(self, docname: str, typ: str | None = None) -> str:
6258
return docname
6359

64-
def prepare_writing(self, docnames: Set[str]) -> None:
65-
self.writer = self._writer_class(self)
66-
6760
def write_doc(self, docname: str, doctree: nodes.document) -> None:
6861
# work around multiple string % tuple issues in docutils;
6962
# replace tuples in attribute values with lists
@@ -79,16 +72,25 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None:
7972
for i, val in enumerate(value):
8073
if isinstance(val, tuple):
8174
value[i] = list(val)
82-
destination = StringOutput(encoding='utf-8')
83-
self.writer.write(doctree, destination)
75+
output = self._translate(doctree)
8476
out_file_name = self.outdir / (docname + self.out_suffix)
8577
out_file_name.parent.mkdir(parents=True, exist_ok=True)
8678
try:
87-
with open(out_file_name, 'w', encoding='utf-8') as f:
88-
f.write(self.writer.output)
79+
out_file_name.write_text(output, encoding='utf-8')
8980
except OSError as err:
9081
logger.warning(__('error writing file %s: %s'), out_file_name, err)
9182

83+
def _translate(self, doctree: nodes.document) -> str:
84+
doctree.settings.newlines = doctree.settings.indents = self.config.xml_pretty
85+
doctree.settings.xml_declaration = True
86+
doctree.settings.doctype_declaration = True
87+
88+
# copied from docutils.writers.docutils_xml.Writer.translate()
89+
# so that we can override the translator class
90+
visitor: XMLTranslator = self.create_translator(doctree)
91+
doctree.walkabout(visitor)
92+
return ''.join(visitor.output)
93+
9294
def finish(self) -> None:
9395
pass
9496

@@ -102,7 +104,8 @@ class PseudoXMLBuilder(XMLBuilder):
102104

103105
out_suffix = '.pseudoxml'
104106

105-
_writer_class = PseudoXMLWriter
107+
def _translate(self, doctree: nodes.document) -> str:
108+
return doctree.pformat()
106109

107110

108111
def setup(app: Sphinx) -> ExtensionMetadata:

sphinx/parsers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def get_transforms(self) -> list[type[Transform]]:
7070
7171
refs: sphinx.io.SphinxStandaloneReader
7272
"""
73-
transforms = super().get_transforms()
73+
transforms = super(RSTParser, RSTParser()).get_transforms()
7474
transforms.remove(SmartQuotes)
7575
return transforms
7676

sphinx/writers/html.py

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

55
from typing import TYPE_CHECKING, cast
66

7-
from docutils.writers.html4css1 import Writer
7+
from docutils.writers import html4css1
88

99
from sphinx.util import logging
1010
from sphinx.writers.html5 import HTML5Translator
@@ -20,7 +20,7 @@
2020
# https://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
2121

2222

23-
class HTMLWriter(Writer): # type: ignore[misc]
23+
class HTMLWriter(html4css1.Writer): # type: ignore[misc]
2424
# override embed-stylesheet default value to False.
2525
settings_default_overrides = {'embed_stylesheet': False}
2626

0 commit comments

Comments
 (0)