From 75987b4eacba1a72417a4003a49eb5a0370bda87 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 02:08:33 +0100 Subject: [PATCH 1/7] Fix docname is None --- sphinx/directives/other.py | 10 +++++-- tests/roots/test-directive-include/bar.txt | 1 + .../roots/test-directive-include/baz/baz.rst | 2 +- tests/test_directive_other.py | 27 +++++++++++-------- 4 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 tests/roots/test-directive-include/bar.txt diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 20fb05b5048..e4fc013c279 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from os.path import abspath +from os.path import abspath, relpath from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -19,7 +19,7 @@ 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 +from sphinx.util.osutil import os_path, path_stabilize, SEP if TYPE_CHECKING: from docutils.nodes import Element, Node @@ -388,6 +388,12 @@ def _insert_input(include_lines, source): # The docname to pass into the source-read event docname = self.env.path2doc(abspath(os_path(source))) + if docname is None: + docname = self.env.docname + # or: + rel_path = path_stabilize(relpath(source, self.env.srcdir)).replace(SEP, '-') + docname = f'include-from-{rel_path}' + # Emit the "source-read" event arg = [text] self.env.app.events.emit("source-read", docname, arg) diff --git a/tests/roots/test-directive-include/bar.txt b/tests/roots/test-directive-include/bar.txt new file mode 100644 index 00000000000..c0fef4c29a1 --- /dev/null +++ b/tests/roots/test-directive-include/bar.txt @@ -0,0 +1 @@ +Text from :file:`bar.txt`. diff --git a/tests/roots/test-directive-include/baz/baz.rst b/tests/roots/test-directive-include/baz/baz.rst index d8207261afc..0b74be0ba9e 100644 --- a/tests/roots/test-directive-include/baz/baz.rst +++ b/tests/roots/test-directive-include/baz/baz.rst @@ -3,4 +3,4 @@ Baz .. include:: foo.rst -Baz was here. \ No newline at end of file +Baz was here. diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index 45bd033045a..d81d8d9347a 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -152,22 +152,27 @@ def test_toctree_twice(app): @pytest.mark.sphinx(testroot='directive-include') def test_include_source_read_event(app): - sources_reported = {} + sources_reported = [] - def source_read_handler(app, doc, source): - sources_reported[doc] = source[0] + def source_read_handler(_app, doc, source): + sources_reported.append((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") + 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." + + found_sources = {filename for filename, s in sources_reported} + assert "index" in found_sources + assert "text.txt" not in found_sources # text was included as literal, no rst parsing + assert "include-from-bar.txt" in found_sources # suffix not in source-suffixes + assert ("baz/baz", "\nBaz was here.") in sources_reported @pytest.mark.sphinx(testroot='directive-include') From 971e18e24acb03cb24addf09d0034a4f4996c060 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:50:15 +0100 Subject: [PATCH 2/7] CHANGES --- CHANGES | 3 +++ sphinx/directives/other.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 64aa161dfe2..5cd64bf1938 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,9 @@ Bugs fixed for sibling files in a subdirectory. Patch by Albert Shih. * #11659: Allow ``?config=...`` in :confval:`mathjax_path`. +* 11620: Provide a generated docname for included sources + in the ``'source-read'`` event, when the included file + does not have an extension contained in :confval:`source_suffix`. Testing ------- diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index e4fc013c279..ea788250bf2 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -389,8 +389,6 @@ def _insert_input(include_lines, source): # The docname to pass into the source-read event docname = self.env.path2doc(abspath(os_path(source))) if docname is None: - docname = self.env.docname - # or: rel_path = path_stabilize(relpath(source, self.env.srcdir)).replace(SEP, '-') docname = f'include-from-{rel_path}' From b083dbec34cd8bfee42934503d8718dee933bb20 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:51:38 +0100 Subject: [PATCH 3/7] imports --- sphinx/directives/other.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index ea788250bf2..f12f686118b 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -19,7 +19,7 @@ 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, path_stabilize, SEP +from sphinx.util.osutil import SEP, os_path, path_stabilize if TYPE_CHECKING: from docutils.nodes import Element, Node From c0189722c02efb1f6c1e7458822d3a1791d4c082 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:36:38 +0100 Subject: [PATCH 4/7] Add new 'include-read' event --- doc/extdev/appapi.rst | 14 ++++++++++++++ sphinx/directives/other.py | 16 +++++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 78e88b06301..d84f41b0da6 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -260,6 +260,20 @@ Here is a more detailed list of these events. .. versionadded:: 0.5 +.. event:: include-read (app, path, content) + + Emitted when a file has been read with the :rst:dir:`include` directive. + The *path* argument is the absolute path of the included file. + 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 :rst:dir:`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 diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index f12f686118b..50e62c4d462 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from os.path import abspath, relpath +from os.path import abspath, relpath, normpath from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -19,7 +19,7 @@ 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 SEP, os_path, path_stabilize +from sphinx.util.osutil import os_path if TYPE_CHECKING: from docutils.nodes import Element, Node @@ -380,21 +380,15 @@ def run(self) -> list[Node]: # 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: text = "\n".join(include_lines[:-2]) - # The docname to pass into the source-read event - docname = self.env.path2doc(abspath(os_path(source))) - if docname is None: - rel_path = path_stabilize(relpath(source, self.env.srcdir)).replace(SEP, '-') - docname = f'include-from-{rel_path}' - # Emit the "source-read" event arg = [text] - self.env.app.events.emit("source-read", docname, arg) + self.env.app.events.emit('include-read', normpath(abspath(source)), arg) text = arg[0] # Split back into lines and reattach the two marker lines @@ -406,7 +400,7 @@ def _insert_input(include_lines, source): 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'): + 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] From f5e3759e1495aa62456371d8b613700e59de3844 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:39:35 +0100 Subject: [PATCH 5/7] lint --- sphinx/directives/other.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 50e62c4d462..8677e9c8cd8 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from os.path import abspath, relpath, normpath +from os.path import abspath, normpath from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -19,7 +19,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 From f4a68a7c175cecc0ed9e0ff669a756e06bec3f56 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:50:25 +0100 Subject: [PATCH 6/7] Updates: - Add to core events - Include the parent docname - Relative path --- doc/extdev/appapi.rst | 11 +++++++---- sphinx/directives/other.py | 16 ++++++++++------ sphinx/events.py | 1 + tests/test_directive_other.py | 29 ++++++++++++++++------------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index d84f41b0da6..270000b6b7e 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -260,10 +260,13 @@ Here is a more detailed list of these events. .. versionadded:: 0.5 -.. event:: include-read (app, path, content) +.. event:: include-read (app, relative_path, parent_docname, content) - Emitted when a file has been read with the :rst:dir:`include` directive. - The *path* argument is the absolute path of the included file. + 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 @@ -272,7 +275,7 @@ Here is a more detailed list of these events. .. versionadded:: 7.2.5 - .. seealso:: The :rst:dir:`include` directive and the :event:`source-read` event. + .. seealso:: The :dudir:`include` directive and the :event:`source-read` event. .. event:: object-description-transform (app, domain, objtype, contentnode) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 8677e9c8cd8..65cd90b6592 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -1,7 +1,8 @@ from __future__ import annotations import re -from os.path import abspath, normpath +from os.path import abspath, relpath +from pathlib import Path from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -372,7 +373,7 @@ 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 @@ -382,12 +383,15 @@ def _insert_input(include_lines, source): # 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]) - # 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('include-read', normpath(abspath(source)), arg) + self.env.app.events.emit('include-read', path, docname, arg) text = arg[0] # Split back into lines and reattach the two marker lines @@ -398,7 +402,7 @@ 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'. + # 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] diff --git a/sphinx/events.py b/sphinx/events.py index c3fdb37c1c7..dad33aa632a 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -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', diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index d81d8d9347a..1feb251c557 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -1,4 +1,5 @@ """Test the other directives.""" +from pathlib import Path import pytest from docutils import nodes @@ -151,13 +152,13 @@ def test_toctree_twice(app): @pytest.mark.sphinx(testroot='directive-include') -def test_include_source_read_event(app): +def test_include_include_read_event(app): sources_reported = [] - def source_read_handler(_app, doc, source): - sources_reported.append((doc, source[0])) + def source_read_handler(_app, relative_path, parent_docname, source): + sources_reported.append((relative_path, parent_docname, source[0])) - app.connect("source-read", source_read_handler) + app.connect("include-read", source_read_handler) text = """\ .. include:: baz/baz.rst :start-line: 4 @@ -168,22 +169,24 @@ def source_read_handler(_app, doc, source): app.env.find_files(app.config, app.builder) restructuredtext.parse(app, text, 'index') - found_sources = {filename for filename, s in sources_reported} - assert "index" in found_sources - assert "text.txt" not in found_sources # text was included as literal, no rst parsing - assert "include-from-bar.txt" in found_sources # suffix not in source-suffixes - assert ("baz/baz", "\nBaz was here.") in sources_reported + 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) From e6284403bd09955d07f7ea7b9f7e04f0b0079dc4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:17:13 +0100 Subject: [PATCH 7/7] CHANGES --- CHANGES | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index af1134e4293..8285143155c 100644 --- a/CHANGES +++ b/CHANGES @@ -28,9 +28,10 @@ Bugs fixed when an object claims to be an instance of ``type``, but is not a class. Patch by James Braza. -* 11620: Provide a generated docname for included sources - in the ``'source-read'`` event, when the included file - does not have an extension contained in :confval:`source_suffix`. +* 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 ------- @@ -146,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.