From 0495ad7273b1d6599e6275f7b0aed8eaceef9cd0 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:31:57 +0100 Subject: [PATCH] Cache publisher for reading documents --- sphinx/builders/__init__.py | 4 +++- sphinx/io.py | 38 ++++++++++---------------------- sphinx/registry.py | 44 +++++++++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index e1817c445da..8e246f0749e 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -464,8 +464,10 @@ def read_doc(self, docname: str) -> None: if path.isfile(docutilsconf): self.env.note_dependency(docutilsconf) + filename = self.env.doc2path(docname) + publisher = self.app.registry.create_publisher(self.app, filename) with sphinx_domains(self.env), rst.default_role(docname, self.config.default_role): - doctree = read_doc(self.app, self.env, self.env.doc2path(docname)) + doctree = read_doc(publisher, docname, filename) # store time of reading, for outdated files detection # (Some filesystems have coarse timestamp resolution; diff --git a/sphinx/io.py b/sphinx/io.py index 936631d517a..5c0ce466da1 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -1,13 +1,11 @@ """Input/Output files""" -import codecs from typing import TYPE_CHECKING, Any, List, Type from docutils import nodes from docutils.core import Publisher from docutils.frontend import Values -from docutils.io import FileInput, Input, NullOutput +from docutils.io import FileInput, Input from docutils.parsers import Parser -from docutils.parsers.rst import Parser as RSTParser from docutils.readers import standalone from docutils.transforms import Transform from docutils.transforms.references import DanglingReferences @@ -20,7 +18,7 @@ from sphinx.transforms.i18n import (Locale, PreserveTranslatableMessages, RemoveTranslatableInline) from sphinx.transforms.references import SphinxDomains -from sphinx.util import UnicodeDecodeErrorHandler, get_filetype, logging +from sphinx.util import UnicodeDecodeErrorHandler, logging from sphinx.util.docutils import LoggingReporter from sphinx.versioning import UIDTransform @@ -153,30 +151,16 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) -def read_doc(app: "Sphinx", env: BuildEnvironment, filename: str) -> nodes.document: +def read_doc(publisher: Publisher, docname: str, filename: str) -> nodes.document: """Parse a document and convert to doctree.""" # set up error_handler for the target document - error_handler = UnicodeDecodeErrorHandler(env.docname) + error_handler = UnicodeDecodeErrorHandler(docname) codecs.register_error('sphinx', error_handler) # type: ignore - reader = SphinxStandaloneReader() - reader.setup(app) - filetype = get_filetype(app.config.source_suffix, filename) - parser = app.registry.create_source_parser(app, filetype) - if parser.__class__.__name__ == 'CommonMarkParser' and parser.settings_spec == (): - # a workaround for recommonmark - # If recommonmark.AutoStrictify is enabled, the parser invokes reST parser - # internally. But recommonmark-0.4.0 does not provide settings_spec for reST - # parser. As a workaround, this copies settings_spec for RSTParser to the - # CommonMarkParser. - parser.settings_spec = RSTParser.settings_spec - - pub = Publisher(reader=reader, - parser=parser, - writer=SphinxDummyWriter(), - source_class=SphinxFileInput, - destination=NullOutput()) - pub.process_programmatic_settings(None, env.settings, None) - pub.set_source(source_path=filename) - pub.publish() - return pub.document + publisher.set_source(source_path=filename) + publisher.publish() + + doctree = publisher.document + # settings get modified in ``write_doctree``; get a local copy + doctree.settings = doctree.settings.copy() + return doctree diff --git a/sphinx/registry.py b/sphinx/registry.py index 0f80109560c..b7728a050cb 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -8,7 +8,8 @@ Union) from docutils import nodes -from docutils.io import Input +from docutils.core import Publisher +from docutils.io import Input, NullOutput from docutils.nodes import Element, Node, TextElement from docutils.parsers import Parser from docutils.parsers.rst import Directive @@ -27,10 +28,11 @@ from sphinx.environment import BuildEnvironment from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError from sphinx.extension import Extension +from sphinx.io import SphinxDummyWriter, SphinxFileInput, SphinxStandaloneReader from sphinx.locale import __ from sphinx.parsers import Parser as SphinxParser from sphinx.roles import XRefRole -from sphinx.util import logging +from sphinx.util import get_filetype, logging from sphinx.util.logging import prefixed_warnings from sphinx.util.typing import RoleFunction, TitleGetter @@ -125,6 +127,9 @@ def __init__(self) -> None: #: additional transforms; list of transforms self.transforms: List[Type[Transform]] = [] + # private cache of Docutils Publishers (file type -> publisher object) + self._publishers: Dict[str, Publisher] = {} + def add_builder(self, builder: Type[Builder], override: bool = False) -> None: logger.debug('[app] adding builder: %r', builder) if not hasattr(builder, 'name'): @@ -461,6 +466,41 @@ def get_envversion(self, app: "Sphinx") -> Dict[str, str]: envversion['sphinx'] = ENV_VERSION return envversion + def create_publiser(self, app: "Sphinx", filename: str) -> Publisher: + filetype = get_filetype(app.config.source_suffix, filename) + try: + return self._publishers[filetype] + except KeyError: + pass + + reader = SphinxStandaloneReader() + reader.setup(app) + + parser = app.registry.create_source_parser(app, filetype) + if parser.__class__.__name__ == 'CommonMarkParser' and parser.settings_spec == (): + # a workaround for recommonmark + # If recommonmark.AutoStrictify is enabled, the parser invokes reST parser + # internally. But recommonmark-0.4.0 does not provide settings_spec for reST + # parser. As a workaround, this copies settings_spec for RSTParser to the + # CommonMarkParser. + from docutils.parsers.rst import Parser as RSTParser + + parser.settings_spec = RSTParser.settings_spec + + pub = Publisher( + reader=reader, + parser=parser, + writer=SphinxDummyWriter(), + source_class=SphinxFileInput, + destination=NullOutput() + ) + # Propagate exceptions by default when used programmatically: + defaults = {"traceback": True, **app.env.settings} + # Set default settings + pub.settings = pub.setup_option_parser(**defaults).get_default_values() # type: ignore + self._publishers[filetype] = pub + return pub + def merge_source_suffix(app: "Sphinx", config: Config) -> None: """Merge any user-specified source_suffix with any added by extensions."""