feat: add the ability to load plugins in config
This commit is contained in:
parent
9cd5d073f2
commit
c2d4929291
7 changed files with 114 additions and 11 deletions
|
@ -13,7 +13,7 @@ feature request][issue]. It's great to hear about new ideas.
|
||||||
|
|
||||||
If you are inclined to do so, you're welcome to [fork][fork] Dotbot, work on
|
If you are inclined to do so, you're welcome to [fork][fork] Dotbot, work on
|
||||||
implementing the feature yourself, and submit a patch. In this case, it's
|
implementing the feature yourself, and submit a patch. In this case, it's
|
||||||
*highly recommended* that you first [open an issue][issue] describing your
|
_highly recommended_ that you first [open an issue][issue] describing your
|
||||||
enhancement to get early feedback on the new feature that you are implementing.
|
enhancement to get early feedback on the new feature that you are implementing.
|
||||||
This will help avoid wasted efforts and ensure that your work is incorporated
|
This will help avoid wasted efforts and ensure that your work is incorporated
|
||||||
into the code base.
|
into the code base.
|
||||||
|
@ -35,7 +35,7 @@ Want to hack on Dotbot? Awesome!
|
||||||
If there are [open issues][issues], you're more than welcome to work on those -
|
If there are [open issues][issues], you're more than welcome to work on those -
|
||||||
this is probably the best way to contribute to Dotbot. If you have your own
|
this is probably the best way to contribute to Dotbot. If you have your own
|
||||||
ideas, that's great too! In that case, before working on substantial changes to
|
ideas, that's great too! In that case, before working on substantial changes to
|
||||||
the code base, it is *highly recommended* that you first [open an issue][issue]
|
the code base, it is _highly recommended_ that you first [open an issue][issue]
|
||||||
describing what you intend to work on.
|
describing what you intend to work on.
|
||||||
|
|
||||||
**Patches are generally submitted as pull requests.** Patches are also
|
**Patches are generally submitted as pull requests.** Patches are also
|
||||||
|
|
23
README.md
23
README.md
|
@ -429,12 +429,27 @@ should do something and return whether or not it completed successfully.
|
||||||
All built-in Dotbot directives are written as plugins that are loaded by
|
All built-in Dotbot directives are written as plugins that are loaded by
|
||||||
default, so those can be used as a reference when writing custom plugins.
|
default, so those can be used as a reference when writing custom plugins.
|
||||||
|
|
||||||
Plugins are loaded using the `--plugin` and `--plugin-dir` options, using
|
|
||||||
either absolute paths or paths relative to the base directory. It is
|
|
||||||
recommended that these options are added directly to the `install` script.
|
|
||||||
|
|
||||||
See [here][plugins] for a current list of plugins.
|
See [here][plugins] for a current list of plugins.
|
||||||
|
|
||||||
|
#### Format
|
||||||
|
|
||||||
|
Plugins can be loaded either by the command-line arguments `--plugin` or
|
||||||
|
`--plugin-dir` or by the `plugins` directive. Each of these take either
|
||||||
|
absolute paths or paths relative to the base directory.
|
||||||
|
|
||||||
|
When using command-line arguments to load multiple plugins you must add
|
||||||
|
one argument for each plugin to be loaded. It is recommended to place
|
||||||
|
these command-line arguments directly in the `install` script.
|
||||||
|
|
||||||
|
The `plugins` config directive is specified as an array of paths to load.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- plugins:
|
||||||
|
- dotbot-plugins/dotbot-template
|
||||||
|
```
|
||||||
|
|
||||||
## Command-line Arguments
|
## Command-line Arguments
|
||||||
|
|
||||||
Dotbot takes a number of command-line arguments; you can run Dotbot with
|
Dotbot takes a number of command-line arguments; you can run Dotbot with
|
||||||
|
|
|
@ -9,7 +9,7 @@ import dotbot
|
||||||
from dotbot.config import ConfigReader, ReadingError
|
from dotbot.config import ConfigReader, ReadingError
|
||||||
from dotbot.dispatcher import Dispatcher, DispatchError, _all_plugins
|
from dotbot.dispatcher import Dispatcher, DispatchError, _all_plugins
|
||||||
from dotbot.messenger import Level, Messenger
|
from dotbot.messenger import Level, Messenger
|
||||||
from dotbot.plugins import Clean, Create, Link, Shell
|
from dotbot.plugins import Clean, Create, Link, Plugins, Shell
|
||||||
from dotbot.util import module
|
from dotbot.util import module
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ def main() -> None:
|
||||||
plugins = []
|
plugins = []
|
||||||
plugin_directories = list(options.plugin_dirs)
|
plugin_directories = list(options.plugin_dirs)
|
||||||
if not options.disable_built_in_plugins:
|
if not options.disable_built_in_plugins:
|
||||||
plugins.extend([Clean, Create, Link, Shell])
|
plugins.extend([Clean, Create, Link, Plugins, Shell])
|
||||||
plugin_paths = []
|
plugin_paths = []
|
||||||
for directory in plugin_directories:
|
for directory in plugin_directories:
|
||||||
plugin_paths.extend(glob.glob(os.path.join(directory, "*.py")))
|
plugin_paths.extend(glob.glob(os.path.join(directory, "*.py")))
|
||||||
|
|
|
@ -37,7 +37,10 @@ class Dispatcher:
|
||||||
self._exit = exit_on_failure
|
self._exit = exit_on_failure
|
||||||
|
|
||||||
def _setup_context(
|
def _setup_context(
|
||||||
self, base_directory: str, options: Optional[Namespace], plugins: Optional[List[Type[Plugin]]]
|
self,
|
||||||
|
base_directory: str,
|
||||||
|
options: Optional[Namespace],
|
||||||
|
plugins: Optional[List[Type[Plugin]]],
|
||||||
) -> None:
|
) -> None:
|
||||||
path = os.path.abspath(os.path.expanduser(base_directory))
|
path = os.path.abspath(os.path.expanduser(base_directory))
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
|
@ -62,6 +65,7 @@ class Dispatcher:
|
||||||
self._context.set_defaults(task[action]) # replace, not update
|
self._context.set_defaults(task[action]) # replace, not update
|
||||||
handled = True
|
handled = True
|
||||||
# keep going, let other plugins handle this if they want
|
# keep going, let other plugins handle this if they want
|
||||||
|
|
||||||
for plugin in self._plugins:
|
for plugin in self._plugins:
|
||||||
if plugin.can_handle(action):
|
if plugin.can_handle(action):
|
||||||
try:
|
try:
|
||||||
|
@ -76,14 +80,25 @@ class Dispatcher:
|
||||||
self._log.error(f"An error was encountered while executing action {action}")
|
self._log.error(f"An error was encountered while executing action {action}")
|
||||||
self._log.debug(str(err))
|
self._log.debug(str(err))
|
||||||
if self._exit:
|
if self._exit:
|
||||||
# There was an execption exit
|
# There was an exception exit
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if action == "plugins":
|
||||||
|
# Create a list of loaded plugin names
|
||||||
|
loaded_plugins = [type(plugin).__name__ for plugin in self._plugins]
|
||||||
|
|
||||||
|
# Load plugins that haven't been loaded yet
|
||||||
|
for plugin_subclass in Plugin.__subclasses__():
|
||||||
|
if type(plugin_subclass).__name__ not in loaded_plugins:
|
||||||
|
self._plugins.append(plugin_subclass(self._context))
|
||||||
|
|
||||||
if not handled:
|
if not handled:
|
||||||
success = False
|
success = False
|
||||||
self._log.error(f"Action {action} not handled")
|
self._log.error(f"Action {action} not handled")
|
||||||
if self._exit:
|
if self._exit:
|
||||||
# Invalid action exit
|
# Invalid action exit
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from dotbot.plugins.clean import Clean
|
from dotbot.plugins.clean import Clean
|
||||||
from dotbot.plugins.create import Create
|
from dotbot.plugins.create import Create
|
||||||
from dotbot.plugins.link import Link
|
from dotbot.plugins.link import Link
|
||||||
|
from dotbot.plugins.plugins import Plugins
|
||||||
from dotbot.plugins.shell import Shell
|
from dotbot.plugins.shell import Shell
|
||||||
|
|
||||||
__all__ = ["Clean", "Create", "Link", "Shell"]
|
__all__ = ["Clean", "Create", "Link", "Plugins", "Shell"]
|
||||||
|
|
47
src/dotbot/plugins/plugins.py
Normal file
47
src/dotbot/plugins/plugins.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dotbot.plugin import Plugin
|
||||||
|
from dotbot.util import module
|
||||||
|
|
||||||
|
|
||||||
|
class Plugins(Plugin):
|
||||||
|
"""
|
||||||
|
Load plugins from a list of paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_directive = "plugins"
|
||||||
|
_has_shown_override_message = False
|
||||||
|
|
||||||
|
def can_handle(self, directive: str) -> bool:
|
||||||
|
return directive == self._directive
|
||||||
|
|
||||||
|
def handle(self, directive: str, data: Any) -> bool:
|
||||||
|
if directive != self._directive:
|
||||||
|
msg = f"plugins cannot handle directive {directive}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
return self._process_plugins(data)
|
||||||
|
|
||||||
|
def _process_plugins(self, data: Any) -> bool:
|
||||||
|
success = True
|
||||||
|
plugin_paths = []
|
||||||
|
for item in data:
|
||||||
|
self._log.lowinfo(f"Loading plugin from {item}")
|
||||||
|
|
||||||
|
plugin_path_globs = glob.glob(os.path.join(item, "*.py"))
|
||||||
|
if not plugin_path_globs:
|
||||||
|
success = False
|
||||||
|
self._log.warning(f"Failed to load plugin from {item}")
|
||||||
|
else:
|
||||||
|
plugin_paths = list(plugin_path_globs)
|
||||||
|
|
||||||
|
for plugin_path in plugin_paths:
|
||||||
|
abspath = os.path.abspath(plugin_path)
|
||||||
|
module.load(abspath)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self._log.info("All commands have been executed")
|
||||||
|
else:
|
||||||
|
self._log.error("Some commands were not successfully executed")
|
||||||
|
return success
|
25
tests/dotbot_plugin_config_file.py
Normal file
25
tests/dotbot_plugin_config_file.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""Test that a plugin can be loaded by config file.
|
||||||
|
|
||||||
|
This file is copied to a location with the name "config_file.py",
|
||||||
|
and is then loaded from within the `test_cli.py` code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import dotbot
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFile(dotbot.Plugin):
|
||||||
|
_directive = "plugin_config_file"
|
||||||
|
|
||||||
|
def can_handle(self, directive: str) -> bool:
|
||||||
|
return directive == self._directive
|
||||||
|
|
||||||
|
def handle(self, directive: str, _data: Any) -> bool:
|
||||||
|
if not self.can_handle(directive):
|
||||||
|
msg = f"ConfigFile cannot handle directive {directive}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
with open(os.path.abspath(os.path.expanduser("~/flag")), "w") as file:
|
||||||
|
file.write("config file plugin loading works")
|
||||||
|
return True
|
Loading…
Reference in a new issue