Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an 'include-read' event #11657

Merged
merged 9 commits into from Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGES
Expand Up @@ -28,6 +28,10 @@ Bugs fixed
when an object claims to be an instance of ``type``,
but is not a class.
Patch by James Braza.
* 11620: Cease emitting :event:`source-read` events for files read via
the :dudir:`include` directive.
* 11620: Add a new :event:`include-read` for observing and transforming
the content of included files via the :dudir:`include` directive.

Testing
-------
Expand Down Expand Up @@ -143,7 +147,7 @@ Features added
* #11572: Improve ``debug`` logging of reasons why files are detected as out of
date.
Patch by Eric Larson.
* #10678: Emit "source-read" events for files read via
* #10678: Emit :event:`source-read` events for files read via
the :dudir:`include` directive.
Patch by Halldor Fannar.
* #11570: Use short names when using :pep:`585` built-in generics.
Expand Down
17 changes: 17 additions & 0 deletions doc/extdev/appapi.rst
Expand Up @@ -260,6 +260,23 @@ Here is a more detailed list of these events.

.. versionadded:: 0.5

.. event:: include-read (app, relative_path, parent_docname, content)

Emitted when a file has been read with the :dudir:`include` directive.
The *relative_path* argument is a :py:class:`~pathlib.Path` object representing
the relative path of the included file from the :term:`source directory`.
The *parent_docname* argument is the name of the document that
contains the :dudir:`include` directive.
The *source* argument is a list whose single element is
the contents of the included file.
You can process the contents and replace this item
to transform the included content,
as with the :event:`source-read` event.

.. versionadded:: 7.2.5

.. seealso:: The :dudir:`include` directive and the :event:`source-read` event.

.. event:: object-description-transform (app, domain, objtype, contentnode)

Emitted when an object description directive has run. The *domain* and
Expand Down
23 changes: 12 additions & 11 deletions sphinx/directives/other.py
@@ -1,7 +1,8 @@
from __future__ import annotations

import re
from os.path import abspath
from os.path import abspath, relpath
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast

from docutils import nodes
Expand All @@ -19,7 +20,6 @@
from sphinx.util.docutils import SphinxDirective
from sphinx.util.matching import Matcher, patfilter
from sphinx.util.nodes import explicit_title_re
from sphinx.util.osutil import os_path

if TYPE_CHECKING:
from docutils.nodes import Element, Node
Expand Down Expand Up @@ -373,24 +373,25 @@ class Include(BaseInclude, SphinxDirective):

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

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

# The docname to pass into the source-read event
docname = self.env.path2doc(abspath(os_path(source)))
# Emit the "source-read" event
path = Path(relpath(abspath(source), start=self.env.srcdir))
docname = self.env.docname

# Emit the "include-read" event
arg = [text]
self.env.app.events.emit("source-read", docname, arg)
self.env.app.events.emit('include-read', path, docname, arg)
text = arg[0]

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

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

Expand Down
1 change: 1 addition & 0 deletions sphinx/events.py
Expand Up @@ -38,6 +38,7 @@ class EventListener(NamedTuple):
'env-before-read-docs': 'env, docnames',
'env-check-consistency': 'env',
'source-read': 'docname, source text',
'include-read': 'relative path, parent docname, source text',
'doctree-read': 'the doctree before being pickled',
'env-merge-info': 'env, read docnames, other env instance',
'missing-reference': 'env, node, contnode',
Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-directive-include/bar.txt
@@ -0,0 +1 @@
Text from :file:`bar.txt`.
2 changes: 1 addition & 1 deletion tests/roots/test-directive-include/baz/baz.rst
Expand Up @@ -3,4 +3,4 @@ Baz

.. include:: foo.rst

Baz was here.
Baz was here.
46 changes: 27 additions & 19 deletions tests/test_directive_other.py
@@ -1,4 +1,5 @@
"""Test the other directives."""
from pathlib import Path

import pytest
from docutils import nodes
Expand Down Expand Up @@ -151,34 +152,41 @@ def test_toctree_twice(app):


@pytest.mark.sphinx(testroot='directive-include')
def test_include_source_read_event(app):
sources_reported = {}

def source_read_handler(app, doc, source):
sources_reported[doc] = source[0]

app.connect("source-read", source_read_handler)
text = (".. include:: baz/baz.rst\n"
" :start-line: 4\n\n"
".. include:: text.txt\n"
" :literal: \n")
def test_include_include_read_event(app):
sources_reported = []

def source_read_handler(_app, relative_path, parent_docname, source):
sources_reported.append((relative_path, parent_docname, source[0]))

app.connect("include-read", source_read_handler)
text = """\
.. include:: baz/baz.rst
:start-line: 4
.. include:: text.txt
:literal:
.. include:: bar.txt
"""
app.env.find_files(app.config, app.builder)
restructuredtext.parse(app, text, 'index')
assert "index" in sources_reported
assert "text.txt" not in sources_reported # text was included as literal, no rst parsing
assert "baz/baz" in sources_reported
assert sources_reported["baz/baz"] == "\nBaz was here."

included_files = {filename.as_posix()
for filename, p, s in sources_reported}
assert 'index.rst' not in included_files # sources don't emit 'include-read'
assert 'baz/baz.rst' in included_files
assert 'text.txt' not in included_files # text was included as literal, no rst parsing
assert 'bar.txt' in included_files # suffix not in source-suffixes
assert (Path('baz/baz.rst'), 'index', '\nBaz was here.') in sources_reported


@pytest.mark.sphinx(testroot='directive-include')
def test_include_source_read_event_nested_includes(app):
def test_include_include_read_event_nested_includes(app):

def source_read_handler(app, doc, source):
def source_read_handler(_app, _relative_path, _parent_docname, source):
text = source[0].replace("#magical", "amazing")
source[0] = text

app.connect("source-read", source_read_handler)
text = (".. include:: baz/baz.rst\n")
app.connect("include-read", source_read_handler)
text = ".. include:: baz/baz.rst\n"
app.env.find_files(app.config, app.builder)
doctree = restructuredtext.parse(app, text, 'index')
assert_node(doctree, addnodes.document)
Expand Down