Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add the ability to load plugins in config #294

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.
This will help avoid wasted efforts and ensure that your work is incorporated
into the code base.
Expand All @@ -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 -
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
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.

**Patches are generally submitted as pull requests.** Patches are also
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.

#### 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

Dotbot takes a number of command-line arguments; you can run Dotbot with
Expand Down
4 changes: 2 additions & 2 deletions src/dotbot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from dotbot.config import ConfigReader, ReadingError
from dotbot.dispatcher import Dispatcher, DispatchError, _all_plugins
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


Expand Down Expand Up @@ -101,7 +101,7 @@ def main() -> None:
plugins = []
plugin_directories = list(options.plugin_dirs)
if not options.disable_built_in_plugins:
plugins.extend([Clean, Create, Link, Shell])
plugins.extend([Clean, Create, Link, Plugins, Shell])
plugin_paths = []
for directory in plugin_directories:
plugin_paths.extend(glob.glob(os.path.join(directory, "*.py")))
Expand Down
19 changes: 17 additions & 2 deletions src/dotbot/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ def __init__(
self._exit = exit_on_failure

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:
path = os.path.abspath(os.path.expanduser(base_directory))
if not os.path.exists(path):
Expand All @@ -62,6 +65,7 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
self._context.set_defaults(task[action]) # replace, not update
handled = True
# keep going, let other plugins handle this if they want

for plugin in self._plugins:
if plugin.can_handle(action):
try:
Expand All @@ -76,14 +80,25 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
self._log.error(f"An error was encountered while executing action {action}")
self._log.debug(str(err))
if self._exit:
# There was an execption exit
# There was an exception exit
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:
success = False
self._log.error(f"Action {action} not handled")
if self._exit:
# Invalid action exit
return False

return success


Expand Down
3 changes: 2 additions & 1 deletion src/dotbot/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dotbot.plugins.clean import Clean
from dotbot.plugins.create import Create
from dotbot.plugins.link import Link
from dotbot.plugins.plugins import Plugins
from dotbot.plugins.shell import Shell

__all__ = ["Clean", "Create", "Link", "Shell"]
__all__ = ["Clean", "Create", "Link", "Plugins", "Shell"]
47 changes: 47 additions & 0 deletions src/dotbot/plugins/plugins.py
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions tests/dotbot_plugin_config_file.py
Original file line number Diff line number Diff line change
@@ -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