From 8c2dc8cbc651957c21176bb5eb5e709c31f3af2e Mon Sep 17 00:00:00 2001 From: Anish Athalye Date: Sat, 7 Dec 2024 18:16:01 -0500 Subject: [PATCH] Make set of plugins available in context This is useful for plugins like dotbot-if [1] that want to instantiate their own Dispatcher. Previously, the Dispatcher found the set of available plugins on its own, but as of b5499c7dc5b300462f3ce1c2a3d9b7a76233b39b, this was changed so that plugins are passed in. Given that it has been over two years since this behavior has been broken/changed, reverting to the previous behavior of having the Dispatcher auto-load plugins might not be ideal, which is why this patch instead makes the set of plugins available via the Context for plugins to use. This was reported in the Dotbot repository [2], and earlier in dotbot-if [3]. dotbot-if is currently using a workaround [4] that was originally implemented in dotbot-ifplatform [5]. [1]: https://github.com/wonderbeyond/dotbot-if [2]: https://github.com/anishathalye/dotbot/issues/339 [3]: https://github.com/wonderbeyond/dotbot-if/issues/1 [4]: https://github.com/wonderbeyond/dotbot-if/pull/2 [5]: https://github.com/ssbanerje/dotbot-ifplatform/commit/e35b5c0d7149d97ccecc8676a07895f35add725c --- dotbot/context.py | 8 ++++++-- dotbot/dispatcher.py | 6 +++--- tests/dotbot_plugin_context_plugin.py | 18 ++++++++++++++++++ tests/test_cli.py | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/dotbot_plugin_context_plugin.py diff --git a/dotbot/context.py b/dotbot/context.py index 3ce9e6e..31e93c9 100644 --- a/dotbot/context.py +++ b/dotbot/context.py @@ -8,11 +8,11 @@ class Context: Contextual data and information for plugins. """ - def __init__(self, base_directory, options=Namespace()): + def __init__(self, base_directory, options=Namespace(), plugins=None): self._base_directory = base_directory self._defaults = {} self._options = options - pass + self._plugins = plugins def set_base_directory(self, base_directory): self._base_directory = base_directory @@ -31,3 +31,7 @@ class Context: def options(self): return copy.deepcopy(self._options) + + def plugins(self): + # shallow copy is ok here + return copy.copy(self._plugins) diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index f89683d..6660d88 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -17,18 +17,18 @@ class Dispatcher: plugins=None, ): self._log = Messenger() - self._setup_context(base_directory, options) + self._setup_context(base_directory, options, plugins) plugins = plugins or [] self._plugins = [plugin(self._context) for plugin in plugins] self._only = only self._skip = skip self._exit = exit_on_failure - def _setup_context(self, base_directory, options): + def _setup_context(self, base_directory, options, plugins): path = os.path.abspath(os.path.expanduser(base_directory)) if not os.path.exists(path): raise DispatchError("Nonexistent base directory") - self._context = Context(path, options) + self._context = Context(path, options, plugins) def dispatch(self, tasks): success = True diff --git a/tests/dotbot_plugin_context_plugin.py b/tests/dotbot_plugin_context_plugin.py new file mode 100644 index 0000000..f1615e9 --- /dev/null +++ b/tests/dotbot_plugin_context_plugin.py @@ -0,0 +1,18 @@ +# https://github.com/anishathalye/dotbot/issues/339 +# plugins should be able to instantiate a Dispatcher with all the plugins + +import dotbot +from dotbot.dispatcher import Dispatcher + + +class Dispatch(dotbot.Plugin): + def can_handle(self, directive): + return directive == "dispatch" + + def handle(self, directive, data): + dispatcher = Dispatcher( + base_directory=self._context.base_directory(), + options=self._context.options(), + plugins=self._context.plugins(), + ) + return dispatcher.dispatch(data) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5868e6f..cc270b4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -170,3 +170,17 @@ def test_disable_builtin_plugins(home, dotfiles, run_dotbot): run_dotbot("--disable-built-in-plugins") assert not os.path.exists(os.path.join(home, ".f")) + + +def test_plugin_context_plugin(capfd, home, dotfiles, run_dotbot): + """Verify that the plugin context is available to plugins.""" + + plugin_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "dotbot_plugin_context_plugin.py" + ) + shutil.copy(plugin_file, os.path.join(dotfiles.directory, "plugin.py")) + dotfiles.write_config([{"dispatch": [{"shell": [{"command": "echo apple", "stdout": True}]}]}]) + run_dotbot("--plugin", os.path.join(dotfiles.directory, "plugin.py")) + + stdout = capfd.readouterr().out.splitlines() + assert any(line.startswith("apple") for line in stdout)