Skip to content

Commit c2d4929

Browse files
committed
feat: add the ability to load plugins in config
1 parent 9cd5d07 commit c2d4929

File tree

7 files changed

+114
-11
lines changed

7 files changed

+114
-11
lines changed

CONTRIBUTING.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ feature request][issue]. It's great to hear about new ideas.
1313

1414
If you are inclined to do so, you're welcome to [fork][fork] Dotbot, work on
1515
implementing the feature yourself, and submit a patch. In this case, it's
16-
*highly recommended* that you first [open an issue][issue] describing your
16+
_highly recommended_ that you first [open an issue][issue] describing your
1717
enhancement to get early feedback on the new feature that you are implementing.
1818
This will help avoid wasted efforts and ensure that your work is incorporated
1919
into the code base.
@@ -35,7 +35,7 @@ Want to hack on Dotbot? Awesome!
3535
If there are [open issues][issues], you're more than welcome to work on those -
3636
this is probably the best way to contribute to Dotbot. If you have your own
3737
ideas, that's great too! In that case, before working on substantial changes to
38-
the code base, it is *highly recommended* that you first [open an issue][issue]
38+
the code base, it is _highly recommended_ that you first [open an issue][issue]
3939
describing what you intend to work on.
4040

4141
**Patches are generally submitted as pull requests.** Patches are also

README.md

+19-4
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,27 @@ should do something and return whether or not it completed successfully.
429429
All built-in Dotbot directives are written as plugins that are loaded by
430430
default, so those can be used as a reference when writing custom plugins.
431431

432-
Plugins are loaded using the `--plugin` and `--plugin-dir` options, using
433-
either absolute paths or paths relative to the base directory. It is
434-
recommended that these options are added directly to the `install` script.
435-
436432
See [here][plugins] for a current list of plugins.
437433

434+
#### Format
435+
436+
Plugins can be loaded either by the command-line arguments `--plugin` or
437+
`--plugin-dir` or by the `plugins` directive. Each of these take either
438+
absolute paths or paths relative to the base directory.
439+
440+
When using command-line arguments to load multiple plugins you must add
441+
one argument for each plugin to be loaded. It is recommended to place
442+
these command-line arguments directly in the `install` script.
443+
444+
The `plugins` config directive is specified as an array of paths to load.
445+
446+
#### Example
447+
448+
```yaml
449+
- plugins:
450+
- dotbot-plugins/dotbot-template
451+
```
452+
438453
## Command-line Arguments
439454

440455
Dotbot takes a number of command-line arguments; you can run Dotbot with

src/dotbot/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dotbot.config import ConfigReader, ReadingError
1010
from dotbot.dispatcher import Dispatcher, DispatchError, _all_plugins
1111
from dotbot.messenger import Level, Messenger
12-
from dotbot.plugins import Clean, Create, Link, Shell
12+
from dotbot.plugins import Clean, Create, Link, Plugins, Shell
1313
from dotbot.util import module
1414

1515

@@ -101,7 +101,7 @@ def main() -> None:
101101
plugins = []
102102
plugin_directories = list(options.plugin_dirs)
103103
if not options.disable_built_in_plugins:
104-
plugins.extend([Clean, Create, Link, Shell])
104+
plugins.extend([Clean, Create, Link, Plugins, Shell])
105105
plugin_paths = []
106106
for directory in plugin_directories:
107107
plugin_paths.extend(glob.glob(os.path.join(directory, "*.py")))

src/dotbot/dispatcher.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ def __init__(
3737
self._exit = exit_on_failure
3838

3939
def _setup_context(
40-
self, base_directory: str, options: Optional[Namespace], plugins: Optional[List[Type[Plugin]]]
40+
self,
41+
base_directory: str,
42+
options: Optional[Namespace],
43+
plugins: Optional[List[Type[Plugin]]],
4144
) -> None:
4245
path = os.path.abspath(os.path.expanduser(base_directory))
4346
if not os.path.exists(path):
@@ -62,6 +65,7 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
6265
self._context.set_defaults(task[action]) # replace, not update
6366
handled = True
6467
# keep going, let other plugins handle this if they want
68+
6569
for plugin in self._plugins:
6670
if plugin.can_handle(action):
6771
try:
@@ -76,14 +80,25 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
7680
self._log.error(f"An error was encountered while executing action {action}")
7781
self._log.debug(str(err))
7882
if self._exit:
79-
# There was an execption exit
83+
# There was an exception exit
8084
return False
85+
86+
if action == "plugins":
87+
# Create a list of loaded plugin names
88+
loaded_plugins = [type(plugin).__name__ for plugin in self._plugins]
89+
90+
# Load plugins that haven't been loaded yet
91+
for plugin_subclass in Plugin.__subclasses__():
92+
if type(plugin_subclass).__name__ not in loaded_plugins:
93+
self._plugins.append(plugin_subclass(self._context))
94+
8195
if not handled:
8296
success = False
8397
self._log.error(f"Action {action} not handled")
8498
if self._exit:
8599
# Invalid action exit
86100
return False
101+
87102
return success
88103

89104

src/dotbot/plugins/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dotbot.plugins.clean import Clean
22
from dotbot.plugins.create import Create
33
from dotbot.plugins.link import Link
4+
from dotbot.plugins.plugins import Plugins
45
from dotbot.plugins.shell import Shell
56

6-
__all__ = ["Clean", "Create", "Link", "Shell"]
7+
__all__ = ["Clean", "Create", "Link", "Plugins", "Shell"]

src/dotbot/plugins/plugins.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import glob
2+
import os
3+
from typing import Any
4+
5+
from dotbot.plugin import Plugin
6+
from dotbot.util import module
7+
8+
9+
class Plugins(Plugin):
10+
"""
11+
Load plugins from a list of paths.
12+
"""
13+
14+
_directive = "plugins"
15+
_has_shown_override_message = False
16+
17+
def can_handle(self, directive: str) -> bool:
18+
return directive == self._directive
19+
20+
def handle(self, directive: str, data: Any) -> bool:
21+
if directive != self._directive:
22+
msg = f"plugins cannot handle directive {directive}"
23+
raise ValueError(msg)
24+
return self._process_plugins(data)
25+
26+
def _process_plugins(self, data: Any) -> bool:
27+
success = True
28+
plugin_paths = []
29+
for item in data:
30+
self._log.lowinfo(f"Loading plugin from {item}")
31+
32+
plugin_path_globs = glob.glob(os.path.join(item, "*.py"))
33+
if not plugin_path_globs:
34+
success = False
35+
self._log.warning(f"Failed to load plugin from {item}")
36+
else:
37+
plugin_paths = list(plugin_path_globs)
38+
39+
for plugin_path in plugin_paths:
40+
abspath = os.path.abspath(plugin_path)
41+
module.load(abspath)
42+
43+
if success:
44+
self._log.info("All commands have been executed")
45+
else:
46+
self._log.error("Some commands were not successfully executed")
47+
return success

tests/dotbot_plugin_config_file.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Test that a plugin can be loaded by config file.
2+
3+
This file is copied to a location with the name "config_file.py",
4+
and is then loaded from within the `test_cli.py` code.
5+
"""
6+
7+
import os.path
8+
from typing import Any
9+
10+
import dotbot
11+
12+
13+
class ConfigFile(dotbot.Plugin):
14+
_directive = "plugin_config_file"
15+
16+
def can_handle(self, directive: str) -> bool:
17+
return directive == self._directive
18+
19+
def handle(self, directive: str, _data: Any) -> bool:
20+
if not self.can_handle(directive):
21+
msg = f"ConfigFile cannot handle directive {directive}"
22+
raise ValueError(msg)
23+
with open(os.path.abspath(os.path.expanduser("~/flag")), "w") as file:
24+
file.write("config file plugin loading works")
25+
return True

0 commit comments

Comments
 (0)