Skip to content

Commit ff18318

Browse files
authored
Add an 'include-read' event (#11657)
1 parent 74329d9 commit ff18318

File tree

7 files changed

+64
-32
lines changed

7 files changed

+64
-32
lines changed

Diff for: CHANGES

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Bugs fixed
2828
when an object claims to be an instance of ``type``,
2929
but is not a class.
3030
Patch by James Braza.
31+
* 11620: Cease emitting :event:`source-read` events for files read via
32+
the :dudir:`include` directive.
33+
* 11620: Add a new :event:`include-read` for observing and transforming
34+
the content of included files via the :dudir:`include` directive.
3135

3236
Testing
3337
-------
@@ -143,7 +147,7 @@ Features added
143147
* #11572: Improve ``debug`` logging of reasons why files are detected as out of
144148
date.
145149
Patch by Eric Larson.
146-
* #10678: Emit "source-read" events for files read via
150+
* #10678: Emit :event:`source-read` events for files read via
147151
the :dudir:`include` directive.
148152
Patch by Halldor Fannar.
149153
* #11570: Use short names when using :pep:`585` built-in generics.

Diff for: doc/extdev/appapi.rst

+17
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,23 @@ Here is a more detailed list of these events.
260260

261261
.. versionadded:: 0.5
262262

263+
.. event:: include-read (app, relative_path, parent_docname, content)
264+
265+
Emitted when a file has been read with the :dudir:`include` directive.
266+
The *relative_path* argument is a :py:class:`~pathlib.Path` object representing
267+
the relative path of the included file from the :term:`source directory`.
268+
The *parent_docname* argument is the name of the document that
269+
contains the :dudir:`include` directive.
270+
The *source* argument is a list whose single element is
271+
the contents of the included file.
272+
You can process the contents and replace this item
273+
to transform the included content,
274+
as with the :event:`source-read` event.
275+
276+
.. versionadded:: 7.2.5
277+
278+
.. seealso:: The :dudir:`include` directive and the :event:`source-read` event.
279+
263280
.. event:: object-description-transform (app, domain, objtype, contentnode)
264281

265282
Emitted when an object description directive has run. The *domain* and

Diff for: sphinx/directives/other.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

33
import re
4-
from os.path import abspath
4+
from os.path import abspath, relpath
5+
from pathlib import Path
56
from typing import TYPE_CHECKING, Any, cast
67

78
from docutils import nodes
@@ -19,7 +20,6 @@
1920
from sphinx.util.docutils import SphinxDirective
2021
from sphinx.util.matching import Matcher, patfilter
2122
from sphinx.util.nodes import explicit_title_re
22-
from sphinx.util.osutil import os_path
2323

2424
if TYPE_CHECKING:
2525
from docutils.nodes import Element, Node
@@ -373,24 +373,25 @@ class Include(BaseInclude, SphinxDirective):
373373

374374
def run(self) -> list[Node]:
375375

376-
# To properly emit "source-read" events from included RST text,
376+
# To properly emit "include-read" events from included RST text,
377377
# we must patch the ``StateMachine.insert_input()`` method.
378378
# In the future, docutils will hopefully offer a way for Sphinx
379379
# to provide the RST parser to use
380380
# when parsing RST text that comes in via Include directive.
381381
def _insert_input(include_lines, source):
382382
# First, we need to combine the lines back into text so that
383-
# we can send it with the source-read event.
383+
# we can send it with the include-read event.
384384
# In docutils 0.18 and later, there are two lines at the end
385385
# that act as markers.
386-
# We must preserve them and leave them out of the source-read event:
386+
# We must preserve them and leave them out of the include-read event:
387387
text = "\n".join(include_lines[:-2])
388388

389-
# The docname to pass into the source-read event
390-
docname = self.env.path2doc(abspath(os_path(source)))
391-
# Emit the "source-read" event
389+
path = Path(relpath(abspath(source), start=self.env.srcdir))
390+
docname = self.env.docname
391+
392+
# Emit the "include-read" event
392393
arg = [text]
393-
self.env.app.events.emit("source-read", docname, arg)
394+
self.env.app.events.emit('include-read', path, docname, arg)
394395
text = arg[0]
395396

396397
# Split back into lines and reattach the two marker lines
@@ -401,8 +402,8 @@ def _insert_input(include_lines, source):
401402
# the *Instance* method and this call is to the *Class* method.
402403
return StateMachine.insert_input(self.state_machine, include_lines, source)
403404

404-
# Only enable this patch if there are listeners for 'source-read'.
405-
if self.env.app.events.listeners.get('source-read'):
405+
# Only enable this patch if there are listeners for 'include-read'.
406+
if self.env.app.events.listeners.get('include-read'):
406407
# See https://github.com/python/mypy/issues/2427 for details on the mypy issue
407408
self.state_machine.insert_input = _insert_input # type: ignore[method-assign]
408409

Diff for: sphinx/events.py

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class EventListener(NamedTuple):
3838
'env-before-read-docs': 'env, docnames',
3939
'env-check-consistency': 'env',
4040
'source-read': 'docname, source text',
41+
'include-read': 'relative path, parent docname, source text',
4142
'doctree-read': 'the doctree before being pickled',
4243
'env-merge-info': 'env, read docnames, other env instance',
4344
'missing-reference': 'env, node, contnode',

Diff for: tests/roots/test-directive-include/bar.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Text from :file:`bar.txt`.

Diff for: tests/roots/test-directive-include/baz/baz.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ Baz
33

44
.. include:: foo.rst
55

6-
Baz was here.
6+
Baz was here.

Diff for: tests/test_directive_other.py

+27-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Test the other directives."""
2+
from pathlib import Path
23

34
import pytest
45
from docutils import nodes
@@ -151,34 +152,41 @@ def test_toctree_twice(app):
151152

152153

153154
@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")
155+
def test_include_include_read_event(app):
156+
sources_reported = []
157+
158+
def source_read_handler(_app, relative_path, parent_docname, source):
159+
sources_reported.append((relative_path, parent_docname, source[0]))
160+
161+
app.connect("include-read", source_read_handler)
162+
text = """\
163+
.. include:: baz/baz.rst
164+
:start-line: 4
165+
.. include:: text.txt
166+
:literal:
167+
.. include:: bar.txt
168+
"""
165169
app.env.find_files(app.config, app.builder)
166170
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+
included_files = {filename.as_posix()
173+
for filename, p, s in sources_reported}
174+
assert 'index.rst' not in included_files # sources don't emit 'include-read'
175+
assert 'baz/baz.rst' in included_files
176+
assert 'text.txt' not in included_files # text was included as literal, no rst parsing
177+
assert 'bar.txt' in included_files # suffix not in source-suffixes
178+
assert (Path('baz/baz.rst'), 'index', '\nBaz was here.') in sources_reported
171179

172180

173181
@pytest.mark.sphinx(testroot='directive-include')
174-
def test_include_source_read_event_nested_includes(app):
182+
def test_include_include_read_event_nested_includes(app):
175183

176-
def source_read_handler(app, doc, source):
184+
def source_read_handler(_app, _relative_path, _parent_docname, source):
177185
text = source[0].replace("#magical", "amazing")
178186
source[0] = text
179187

180-
app.connect("source-read", source_read_handler)
181-
text = (".. include:: baz/baz.rst\n")
188+
app.connect("include-read", source_read_handler)
189+
text = ".. include:: baz/baz.rst\n"
182190
app.env.find_files(app.config, app.builder)
183191
doctree = restructuredtext.parse(app, text, 'index')
184192
assert_node(doctree, addnodes.document)

0 commit comments

Comments
 (0)