Skip to content

Commit 88f7fa9

Browse files
authored
Add _parse_str_to_doctree() helper method (#13673)
1 parent 97f2fb2 commit 88f7fa9

File tree

4 files changed

+89
-45
lines changed

4 files changed

+89
-45
lines changed

sphinx/testing/restructuredtext.py

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
from typing import TYPE_CHECKING
44

5-
from docutils import nodes
6-
7-
from sphinx.io import SphinxBaseReader
85
from sphinx.parsers import RSTParser
9-
from sphinx.transforms import SphinxTransformer
10-
from sphinx.util.docutils import LoggingReporter, _get_settings, sphinx_domains
6+
from sphinx.util.docutils import _parse_str_to_doctree
117

128
if TYPE_CHECKING:
9+
from docutils import nodes
10+
1311
from sphinx.application import Sphinx
1412

1513

@@ -20,49 +18,29 @@ def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document:
2018
registry = app.registry
2119
srcdir = app.srcdir
2220

23-
source_path = str(srcdir / f'{docname}.rst')
24-
2521
# Get settings
2622
settings_overrides = {
23+
'env': env,
2724
'gettext_compact': True,
2825
'input_encoding': 'utf-8',
2926
'output_encoding': 'unicode',
3027
'traceback': True,
3128
}
32-
settings = _get_settings(SphinxBaseReader, RSTParser, defaults=settings_overrides)
33-
settings._source = source_path
34-
settings.env = env
3529

3630
# Create parser
3731
parser = RSTParser()
3832
parser._config = config
3933
parser._env = env
4034

41-
# Create root document node
42-
reporter = LoggingReporter(
43-
source_path,
44-
settings.report_level,
45-
settings.halt_level,
46-
settings.debug,
47-
settings.error_encoding_error_handler,
48-
)
49-
document = nodes.document(settings, reporter, source=source_path)
50-
document.note_source(source_path, -1)
51-
52-
# substitute transformer
53-
document.transformer = transformer = SphinxTransformer(document)
54-
transformer.add_transforms(SphinxBaseReader().get_transforms())
55-
transformer.add_transforms(registry.get_transforms())
56-
transformer.add_transforms(parser.get_transforms())
57-
5835
env.current_document.docname = docname
5936
try:
60-
with sphinx_domains(env):
61-
parser.parse(text, document)
62-
document.current_source = document.current_line = None
63-
64-
transformer.apply_transforms()
37+
return _parse_str_to_doctree(
38+
text,
39+
filename=srcdir / f'{docname}.rst',
40+
default_settings=settings_overrides,
41+
env=env,
42+
parser=parser,
43+
transforms=registry.get_transforms(),
44+
)
6545
finally:
6646
env.current_document.docname = ''
67-
68-
return document

sphinx/transforms/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from sphinx.deprecation import _deprecation_warning
1919
from sphinx.locale import _, __
2020
from sphinx.util import logging
21-
from sphinx.util.docutils import new_document
2221
from sphinx.util.i18n import format_date
2322
from sphinx.util.nodes import apply_source_workaround, is_smartquotable
2423

@@ -97,6 +96,8 @@ def apply_transforms(self) -> None:
9796
else:
9897
# wrap the target node by document node during transforming
9998
try:
99+
from sphinx.util.docutils import new_document
100+
100101
document = new_document('')
101102
if self.env:
102103
document.settings.env = self.env

sphinx/util/docutils.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import re
77
import warnings
8-
from contextlib import contextmanager
8+
from contextlib import contextmanager, nullcontext
99
from copy import copy
1010
from pathlib import Path
1111
from typing import TYPE_CHECKING
@@ -15,12 +15,15 @@
1515
from docutils.frontend import OptionParser
1616
from docutils.io import FileOutput
1717
from docutils.parsers.rst import Directive, directives, roles
18+
from docutils.readers import standalone
1819
from docutils.statemachine import StateMachine
20+
from docutils.transforms.references import DanglingReferences
1921
from docutils.utils import Reporter, unescape
2022

2123
from sphinx.errors import SphinxError
2224
from sphinx.locale import __
23-
from sphinx.util import logging
25+
from sphinx.transforms import SphinxTransformer
26+
from sphinx.util import logging, rst
2427
from sphinx.util.parsing import nested_parse_to_nodes
2528

2629
logger = logging.getLogger(__name__)
@@ -36,8 +39,10 @@
3639
from docutils import Component
3740
from docutils.frontend import Values
3841
from docutils.nodes import Element, Node, system_message
42+
from docutils.parsers import Parser
3943
from docutils.parsers.rst.states import Inliner
4044
from docutils.statemachine import State, StringList
45+
from docutils.transforms import Transform
4146

4247
from sphinx.builders import Builder
4348
from sphinx.config import Config
@@ -69,6 +74,13 @@ def __call__(
6974
) -> tuple[RoleFunction | None, list[system_message]]: ...
7075

7176

77+
_READER_TRANSFORMS = [
78+
transform
79+
for transform in standalone.Reader().get_transforms()
80+
if transform is not DanglingReferences
81+
]
82+
83+
7284
additional_nodes: set[type[Element]] = set()
7385

7486

@@ -821,6 +833,55 @@ def new_document(source_path: str, settings: Any = None) -> nodes.document:
821833
return document
822834

823835

836+
def _parse_str_to_doctree(
837+
content: str,
838+
*,
839+
filename: Path,
840+
default_role: str = '',
841+
default_settings: Mapping[str, Any],
842+
env: BuildEnvironment,
843+
parser: Parser,
844+
transforms: Sequence[type[Transform]] = (),
845+
) -> nodes.document:
846+
env.current_document._parser = parser
847+
848+
# Propagate exceptions by default when used programmatically:
849+
defaults = {'traceback': True, **default_settings}
850+
settings = _get_settings(standalone.Reader, parser, defaults=defaults)
851+
settings._source = str(filename)
852+
853+
# Create root document node
854+
reporter = LoggingReporter(
855+
source=str(filename),
856+
report_level=settings.report_level,
857+
halt_level=settings.halt_level,
858+
debug=settings.debug,
859+
error_handler=settings.error_encoding_error_handler,
860+
)
861+
document = nodes.document(settings, reporter, source=str(filename))
862+
document.note_source(str(filename), -1)
863+
864+
# substitute transformer
865+
document.transformer = transformer = SphinxTransformer(document)
866+
transformer.add_transforms(_READER_TRANSFORMS)
867+
transformer.add_transforms(transforms)
868+
transformer.add_transforms(parser.get_transforms())
869+
870+
if default_role:
871+
default_role_cm = rst.default_role(env.current_document.docname, default_role)
872+
else:
873+
default_role_cm = nullcontext() # type: ignore[assignment]
874+
with sphinx_domains(env), default_role_cm:
875+
# parse content to abstract syntax tree
876+
parser.parse(content, document)
877+
document.current_source = document.current_line = None
878+
879+
# run transforms
880+
transformer.apply_transforms()
881+
882+
return document
883+
884+
824885
def _get_settings(
825886
*components: Component | type[Component],
826887
defaults: Mapping[str, Any],

tests/test_directives/test_directive_object_description.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
from docutils import nodes
1010

1111
from sphinx import addnodes
12-
from sphinx.io import _create_publisher
1312
from sphinx.testing import restructuredtext
14-
from sphinx.util.docutils import sphinx_domains
13+
from sphinx.util.docutils import _parse_str_to_doctree
1514

1615
if TYPE_CHECKING:
1716
from sphinx.application import Sphinx
@@ -24,15 +23,20 @@ def _doctree_for_test(
2423
) -> nodes.document:
2524
config = app.config
2625
registry = app.registry
26+
27+
filename = env.doc2path(docname)
28+
content = filename.read_text(encoding='utf-8')
29+
2730
env.prepare_settings(docname)
2831
parser = registry.create_source_parser('restructuredtext', config=config, env=env)
29-
publisher = _create_publisher(
30-
env=env, parser=parser, transforms=registry.get_transforms()
32+
return _parse_str_to_doctree(
33+
content,
34+
filename=env.doc2path(docname),
35+
default_settings={'env': env},
36+
env=env,
37+
parser=parser,
38+
transforms=registry.get_transforms(),
3139
)
32-
with sphinx_domains(env):
33-
publisher.set_source(source_path=str(env.doc2path(docname)))
34-
publisher.publish()
35-
return publisher.document
3640

3741

3842
@pytest.mark.sphinx('text', testroot='object-description-sections')

0 commit comments

Comments
 (0)