mirror of
1
0
Fork 0

Separate module importing from plugin identification

This change allows the test framework to reliably specify
which plugins to load and use within the same process.

Previously, plugins were loaded by importing files and then
accessing the Plugin class' list of subclasses.
Now, it's possible to run dotbot multiple times without
plugins accruing across runs with different configurations
and CLI arguments.

In addition, this fixes some circular imports that were
previously avoided because plugins were imported in a function.
This commit is contained in:
Kurt McKee 2022-04-30 20:19:22 -05:00
parent a8dd89f48f
commit b5499c7dc5
7 changed files with 38 additions and 25 deletions

View File

@ -1,4 +1,4 @@
import os, glob
import glob
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
@ -6,6 +6,7 @@ from .config import ConfigReader, ReadingError
from .dispatcher import Dispatcher, DispatchError
from .messenger import Messenger
from .messenger import Level
from .plugins import Clean, Create, Link, Shell
from .util import module
import dotbot
@ -118,9 +119,10 @@ def main():
else:
log.use_color(sys.stdout.isatty())
plugins = []
plugin_directories = list(options.plugin_dirs)
if not options.disable_built_in_plugins:
from .plugins import Clean, Create, Link, Shell
plugins.extend([Clean, Create, Link, Shell])
plugin_paths = []
for directory in plugin_directories:
for plugin_path in glob.glob(os.path.join(directory, "*.py")):
@ -129,7 +131,7 @@ def main():
plugin_paths.append(plugin_path)
for plugin_path in plugin_paths:
abspath = os.path.abspath(plugin_path)
module.load(abspath)
plugins.extend(module.load(abspath))
if not options.config_file:
log.error("No configuration file specified")
exit(1)
@ -151,6 +153,7 @@ def main():
skip=options.skip,
exit_on_failure=options.exit_on_failure,
options=options,
plugins=plugins,
)
success = dispatcher.dispatch(tasks)
if success:

View File

@ -7,11 +7,12 @@ from .context import Context
class Dispatcher(object):
def __init__(
self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace()
self, base_directory, only=None, skip=None, exit_on_failure=False, options=Namespace(), plugins=None,
):
self._log = Messenger()
self._setup_context(base_directory, options)
self._load_plugins()
plugins = plugins or []
self._plugins = [plugin(self._context) for plugin in plugins]
self._only = only
self._skip = skip
self._exit = exit_on_failure
@ -65,9 +66,6 @@ class Dispatcher(object):
return False
return success
def _load_plugins(self):
self._plugins = [plugin(self._context) for plugin in Plugin.__subclasses__()]
class DispatchError(Exception):
pass

View File

@ -1,10 +1,10 @@
import os
import sys
import dotbot
from ..plugin import Plugin
class Clean(dotbot.Plugin):
class Clean(Plugin):
"""
Cleans broken symbolic links.
"""

View File

@ -1,8 +1,9 @@
import os
import dotbot
from ..plugin import Plugin
class Create(dotbot.Plugin):
class Create(Plugin):
"""
Create empty paths.
"""

View File

@ -2,11 +2,12 @@ import os
import sys
import glob
import shutil
import dotbot
import dotbot.util
from ..plugin import Plugin
from ..util import shell_command
class Link(dotbot.Plugin):
class Link(Plugin):
"""
Symbolically links dotfiles.
"""
@ -139,7 +140,7 @@ class Link(dotbot.Plugin):
return success
def _test_success(self, command):
ret = dotbot.util.shell_command(command, cwd=self._context.base_directory())
ret = shell_command(command, cwd=self._context.base_directory())
if ret != 0:
self._log.debug("Test '%s' returned false" % command)
return ret == 0

View File

@ -1,10 +1,8 @@
import os
import subprocess
import dotbot
import dotbot.util
from ..plugin import Plugin
from ..util import shell_command
class Shell(dotbot.Plugin):
class Shell(Plugin):
"""
Run arbitrary shell commands.
"""
@ -50,7 +48,7 @@ class Shell(dotbot.Plugin):
self._log.lowinfo("%s [%s]" % (msg, cmd))
stdout = options.get("stdout", stdout)
stderr = options.get("stderr", stderr)
ret = dotbot.util.shell_command(
ret = shell_command(
cmd,
cwd=self._context.base_directory(),
enable_stdin=stdin,

View File

@ -1,4 +1,7 @@
import sys, os.path
import os
import sys
from dotbot.plugin import Plugin
# We keep references to loaded modules so they don't get garbage collected.
loaded_modules = []
@ -7,8 +10,17 @@ loaded_modules = []
def load(path):
basename = os.path.basename(path)
module_name, extension = os.path.splitext(basename)
plugin = load_module(module_name, path)
loaded_modules.append(plugin)
loaded_module = load_module(module_name, path)
plugins = []
for name in dir(loaded_module):
possible_plugin = getattr(loaded_module, name)
try:
if issubclass(possible_plugin, Plugin) and possible_plugin is not Plugin:
plugins.append(possible_plugin)
except TypeError:
pass
loaded_modules.append(loaded_module)
return plugins
if sys.version_info >= (3, 5):