From 25497721a3b9c3e917cd0398b0fd09dd7d539fcf Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Sun, 10 Sep 2023 10:07:49 -0500 Subject: [PATCH] Make Dispatcher a singleton after first instantiation This allows plugins to re-instantiate the Dispatcher (or, now, use the instance cached in `current_dispatcher`) without knowing what plugins have been loaded by dotbot. This fixes plugins that want to dispatch to other plugins. Fixes #339 --- dotbot/dispatcher.py | 18 ++++++++++++++++++ tests/conftest.py | 7 +++++++ tests/test_dispatcher.py | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/test_dispatcher.py diff --git a/dotbot/dispatcher.py b/dotbot/dispatcher.py index f89683d..334903f 100644 --- a/dotbot/dispatcher.py +++ b/dotbot/dispatcher.py @@ -5,8 +5,23 @@ from .context import Context from .messenger import Messenger from .plugin import Plugin +current_dispatcher = None + class Dispatcher: + def __new__(cls, *args, **kwargs): + # After dotbot instantiates this class, the instance will be cached. + # Subsequent instantiations (say, by plugins) will return the same instance. + # This is needed because plugins don't have access to the entire configuration + # (for example, they won't know which plugins have been loaded). + # This ensures a consistent configuration is used. + global current_dispatcher + if current_dispatcher is None: + instance = object.__new__(cls) + instance.is_initialized = False + current_dispatcher = instance + return current_dispatcher + def __init__( self, base_directory, @@ -16,6 +31,9 @@ class Dispatcher: options=Namespace(), plugins=None, ): + if self.is_initialized: + return + self.is_initialized = True self._log = Messenger() self._setup_context(base_directory, options) plugins = plugins or [] diff --git a/tests/conftest.py b/tests/conftest.py index 2ede2e5..a74b2fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ import pytest import yaml import dotbot.cli +import dotbot.dispatcher def get_long_path(path): @@ -312,3 +313,9 @@ def run_dotbot(dotfiles): dotbot.cli.main() yield runner + + +@pytest.fixture(autouse=True) +def reset_current_dispatcher(): + yield + dotbot.dispatcher.current_dispatcher = None diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py new file mode 100644 index 0000000..65c691d --- /dev/null +++ b/tests/test_dispatcher.py @@ -0,0 +1,22 @@ +import dotbot.dispatcher + + +def test_dispatcher_instantiation(home, dotfiles, run_dotbot): + """Verify that the dispatcher caches itself as a singleton.""" + + assert dotbot.dispatcher.current_dispatcher is None + + dotfiles.write_config([]) + run_dotbot() + + # Verify the dispatcher has been cached. + assert dotbot.dispatcher.current_dispatcher is not None + + existing_id = id(dotbot.dispatcher.current_dispatcher) + new_instance = dotbot.dispatcher.Dispatcher("bogus") + + # Verify the new and existing instances are the same. + assert existing_id == id(new_instance) + + # Verify the singleton was not overridden. + assert existing_id == id(dotbot.dispatcher.current_dispatcher)