From 666d764a3f115af044c56c5a0adcee5c6de48501 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Fri, 15 Dec 2023 17:05:08 +0100 Subject: [PATCH] Add `File.generated` factory method for files -it populates `generated_by` automatically as well, based on the currently running plugin event. --- mkdocs/plugins.py | 7 ++- mkdocs/structure/files.py | 67 +++++++++++++++++++++++++++- mkdocs/tests/structure/file_tests.py | 17 +++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py index f38bbe7f..1992cbe6 100644 --- a/mkdocs/plugins.py +++ b/mkdocs/plugins.py @@ -495,6 +495,8 @@ class PluginCollection(dict, MutableMapping[str, BasePlugin]): by calling `run_event`. """ + _current_plugin: str | None + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.events: dict[str, list[Callable]] = {k: [] for k in EVENTS} @@ -553,9 +555,9 @@ class PluginCollection(dict, MutableMapping[str, BasePlugin]): """ pass_item = item is not None for method in self.events[name]: + self._current_plugin = self._event_origins.get(method, '') if log.getEffectiveLevel() <= logging.DEBUG: - plugin_name = self._event_origins.get(method, '') - log.debug(f"Running `{name}` event from plugin '{plugin_name}'") + log.debug(f"Running `{name}` event from plugin '{self._current_plugin}'") if pass_item: result = method(item, **kwargs) else: @@ -563,6 +565,7 @@ class PluginCollection(dict, MutableMapping[str, BasePlugin]): # keep item if method returned `None` if result is not None: item = result + self._current_plugin = None return item def on_startup(self, *, command: Literal['build', 'gh-deploy', 'serve'], dirty: bool) -> None: diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py index 5dae027b..2b81a0da 100644 --- a/mkdocs/structure/files.py +++ b/mkdocs/structure/files.py @@ -9,7 +9,7 @@ import shutil import warnings from functools import cached_property from pathlib import PurePath -from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Sequence +from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Sequence, overload from urllib.parse import quote as urlquote import pathspec @@ -224,7 +224,9 @@ class File: """Whether the file will be excluded from the built site.""" generated_by: str | None = None - """If not None, indicates that a plugin generated this file on the fly.""" + """If not None, indicates that a plugin generated this file on the fly. + + The value is the plugin's entrypoint name and can be used to find the plugin by key in the PluginCollection.""" _content: str | bytes | None = None """If set, the file's content will be read from here. @@ -252,6 +254,67 @@ class File: page: Page | None = None + @overload + @classmethod + def generated( + cls, + config: MkDocsConfig, + src_uri: str, + *, + content: str | bytes, + inclusion: InclusionLevel = InclusionLevel.UNDEFINED, + ) -> File: + """ + Create a virtual file backed by in-memory content. + + It will pretend to be a file in the docs dir at `src_uri`. + """ + + @overload + @classmethod + def generated( + cls, + config: MkDocsConfig, + src_uri: str, + *, + abs_src_path: str, + inclusion: InclusionLevel = InclusionLevel.UNDEFINED, + ) -> File: + """ + Create a virtual file backed by a physical temporary file at `abs_src_path`. + + It will pretend to be a file in the docs dir at `src_uri`. + """ + + @classmethod + def generated( + cls, + config: MkDocsConfig, + src_uri: str, + *, + content: str | bytes | None = None, + abs_src_path: str | None = None, + inclusion: InclusionLevel = InclusionLevel.UNDEFINED, + ) -> File: + """ + Create a virtual file, backed either by in-memory `content` or by a file at `abs_src_path`. + + It will pretend to be a file in the docs dir at `src_uri`. + """ + if (content is None) == (abs_src_path is None): + raise TypeError("File must have exactly one of 'content' or 'abs_src_path'") + f = cls( + src_uri, + src_dir=None, + dest_dir=config.site_dir, + use_directory_urls=config.use_directory_urls, + inclusion=inclusion, + ) + f.generated_by = config.plugins._current_plugin or '' + f.abs_src_path = abs_src_path + f._content = content + return f + def __init__( self, path: str, diff --git a/mkdocs/tests/structure/file_tests.py b/mkdocs/tests/structure/file_tests.py index 36703176..0d7d1a3c 100644 --- a/mkdocs/tests/structure/file_tests.py +++ b/mkdocs/tests/structure/file_tests.py @@ -293,6 +293,23 @@ class TestFiles(PathAssertionMixin, unittest.TestCase): self.assertEqual(f.content_string, 'вміст') self.assertEqual(f.edit_uri, None) + @tempdir(files={'x.md': 'вміст'}) + def test_generated_file_constructor(self, tdir) -> None: + config = load_config(site_dir='/path/to/site', use_directory_urls=False) + config.plugins._current_plugin = 'foo' + for f in [ + File.generated(config, 'foo/bar.md', content='вміст'), + File.generated(config, 'foo/bar.md', content='вміст'.encode()), + File.generated(config, 'foo/bar.md', abs_src_path=os.path.join(tdir, 'x.md')), + ]: + self.assertEqual(f.src_uri, 'foo/bar.md') + self.assertIsNone(f.src_dir) + self.assertEqual(f.dest_uri, 'foo/bar.html') + self.assertPathsEqual(f.abs_dest_path, os.path.abspath('/path/to/site/foo/bar.html')) + self.assertEqual(f.content_string, 'вміст') + self.assertEqual(f.content_bytes, 'вміст'.encode()) + self.assertEqual(f.edit_uri, None) + def test_files(self): fs = [ File('index.md', '/path/to/docs', '/path/to/site', use_directory_urls=True),