Skip to content

Commit 0bad447

Browse files
halldorfannarAA-Turnerpicnixz
authored
Emit "source-read" events for files read via the include directive (#11510)
Co-authored-by: Adam Turner <[email protected]> Co-authored-by: picnixz <[email protected]>
1 parent 6cb783c commit 0bad447

File tree

7 files changed

+86
-0
lines changed

7 files changed

+86
-0
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Features added
4848
* #11572: Improve ``debug`` logging of reasons why files are detected as out of
4949
date.
5050
Patch by Eric Larson.
51+
* #10678: Emit "source-read" events for files read via
52+
the :dudir:`include` directive.
53+
Patch by Halldor Fannar.
5154

5255
Bugs fixed
5356
----------

sphinx/directives/other.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
99
from docutils.parsers.rst.directives.misc import Class
1010
from docutils.parsers.rst.directives.misc import Include as BaseInclude
11+
from docutils.statemachine import StateMachine
1112

1213
from sphinx import addnodes
1314
from sphinx.domains.changeset import VersionChange # noqa: F401 # for compatibility
@@ -17,6 +18,7 @@
1718
from sphinx.util.docutils import SphinxDirective
1819
from sphinx.util.matching import Matcher, patfilter
1920
from sphinx.util.nodes import explicit_title_re
21+
from sphinx.util.osutil import os_path
2022

2123
if TYPE_CHECKING:
2224
from docutils.nodes import Element, Node
@@ -369,6 +371,40 @@ class Include(BaseInclude, SphinxDirective):
369371
"""
370372

371373
def run(self) -> list[Node]:
374+
375+
# To properly emit "source-read" events from included RST text,
376+
# we must patch the ``StateMachine.insert_input()`` method.
377+
# In the future, docutils will hopefully offer a way for Sphinx
378+
# to provide the RST parser to use
379+
# when parsing RST text that comes in via Include directive.
380+
def _insert_input(include_lines, path):
381+
# First, we need to combine the lines back into text so that
382+
# we can send it with the source-read event.
383+
# In docutils 0.18 and later, there are two lines at the end
384+
# that act as markers.
385+
# We must preserve them and leave them out of the source-read event:
386+
text = "\n".join(include_lines[:-2])
387+
388+
# The docname to pass into the source-read event
389+
docname = self.env.path2doc(os_path(path))
390+
# Emit the "source-read" event
391+
arg = [text]
392+
self.env.app.events.emit("source-read", docname, arg)
393+
text = arg[0]
394+
395+
# Split back into lines and reattach the two marker lines
396+
include_lines = text.splitlines() + include_lines[-2:]
397+
398+
# Call the parent implementation.
399+
# Note that this snake does not eat its tail because we patch
400+
# the *Instance* method and this call is to the *Class* method.
401+
return StateMachine.insert_input(self.state_machine, include_lines, path)
402+
403+
# Only enable this patch if there are listeners for 'source-read'.
404+
if self.env.app.events.listeners.get('source-read'):
405+
# See https://github.com/python/mypy/issues/2427 for details on the mypy issue
406+
self.state_machine.insert_input = _insert_input # type: ignore[method-assign]
407+
372408
if self.arguments[0].startswith('<') and \
373409
self.arguments[0].endswith('>'):
374410
# docutils "standard" includes, do not do path processing
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Baz
2+
===
3+
4+
.. include:: foo.rst
5+
6+
Baz was here.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
project = 'test-directive-include'
2+
exclude_patterns = ['_build']
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The #magical foo.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is plain text.

tests/test_directive_other.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,40 @@ def test_toctree_twice(app):
148148
assert_node(doctree[0][0],
149149
entries=[(None, 'foo'), (None, 'foo')],
150150
includefiles=['foo', 'foo'])
151+
152+
153+
@pytest.mark.sphinx(testroot='directive-include')
154+
def test_include_source_read_event(app):
155+
sources_reported = {}
156+
157+
def source_read_handler(app, doc, source):
158+
sources_reported[doc] = source[0]
159+
160+
app.connect("source-read", source_read_handler)
161+
text = (".. include:: baz/baz.rst\n"
162+
" :start-line: 4\n\n"
163+
".. include:: text.txt\n"
164+
" :literal: \n")
165+
app.env.find_files(app.config, app.builder)
166+
restructuredtext.parse(app, text, 'index')
167+
assert "index" in sources_reported
168+
assert "text.txt" not in sources_reported # text was included as literal, no rst parsing
169+
assert "baz/baz" in sources_reported
170+
assert sources_reported["baz/baz"] == "\nBaz was here."
171+
172+
173+
@pytest.mark.sphinx(testroot='directive-include')
174+
def test_include_source_read_event_nested_includes(app):
175+
176+
def source_read_handler(app, doc, source):
177+
text = source[0].replace("#magical", "amazing")
178+
source[0] = text
179+
180+
app.connect("source-read", source_read_handler)
181+
text = (".. include:: baz/baz.rst\n")
182+
app.env.find_files(app.config, app.builder)
183+
doctree = restructuredtext.parse(app, text, 'index')
184+
assert_node(doctree, addnodes.document)
185+
assert len(doctree.children) == 3
186+
assert_node(doctree.children[1], nodes.paragraph)
187+
assert doctree.children[1].rawsource == "The amazing foo."

0 commit comments

Comments
 (0)