Skip to content

Commit 2fdea89

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

File tree

7 files changed

+145
-10
lines changed

7 files changed

+145
-10
lines changed

CONTRIBUTING.md

Lines changed: 41 additions & 2 deletions
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
@@ -50,6 +50,45 @@ demonstrate that the bug is fixed (or that the feature works).
5050
See the [Dotbot development guide][development] to learn how to run the tests,
5151
type checking, and more.
5252

53+
When preparing a patch, it's recommended that you add unit tests
54+
that demonstrate the bug is fixed (or that the feature works).
55+
You can run the tests on your local machine by installing the `dev` extras.
56+
The steps below do this using a virtual environment:
57+
58+
```shell
59+
# Create a local virtual environment
60+
$ python -m venv .venv
61+
62+
# Activate the virtual environment
63+
# Cygwin, Linux, and MacOS:
64+
$ . .venv/bin/activate
65+
# Windows Powershell:
66+
$ & .venv\Scripts\Activate.ps1
67+
68+
# Update pip and setuptools
69+
(.venv) $ python -m pip install -U pip setuptools
70+
71+
# Install dotbot and its development dependencies
72+
(.venv) $ python -m pip install -e .[dev]
73+
74+
# Run the unit tests
75+
(.venv) $ tox
76+
```
77+
78+
If you prefer to run the tests in an isolated container using Docker, you can
79+
do so with the following:
80+
81+
```shell
82+
docker run -it --rm -v "${PWD}:/dotbot" -w /dotbot python:3.10-alpine /bin/sh
83+
```
84+
85+
If the machine you are running Docker on has SELinux in the enforcing state, you
86+
will have to disable that on the container. This can be done by adding
87+
`--security-opt label:disable` to the above command.
88+
89+
After spawning the container, follow the same instructions as above (create a
90+
virtualenv, ..., run the tests).
91+
5392
---
5493

5594
If you have any questions about anything, feel free to [ask][email]!

README.md

Lines changed: 19 additions & 4 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
6262
self._context.set_defaults(task[action]) # replace, not update
6363
handled = True
6464
# keep going, let other plugins handle this if they want
65+
6566
for plugin in self._plugins:
6667
if plugin.can_handle(action):
6768
try:
@@ -76,14 +77,27 @@ def dispatch(self, tasks: List[Dict[str, Any]]) -> bool:
7677
self._log.error(f"An error was encountered while executing action {action}")
7778
self._log.debug(str(err))
7879
if self._exit:
79-
# There was an execption exit
80+
# There was an exception exit
8081
return False
82+
83+
if action == "plugins":
84+
# Create a list of loaded plugin names
85+
loaded_plugins = [
86+
plugin.__class__.__name__ for plugin in self._plugins
87+
]
88+
89+
# Load plugins that haven't been loaded yet
90+
for plugin in Plugin.__subclasses__():
91+
if plugin.__name__ not in loaded_plugins:
92+
self._plugins.append(plugin(self._context))
93+
8194
if not handled:
8295
success = False
8396
self._log.error(f"Action {action} not handled")
8497
if self._exit:
8598
# Invalid action exit
8699
return False
100+
87101
return success
88102

89103

src/dotbot/plugins/__init__.py

Lines changed: 2 additions & 1 deletion
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

Lines changed: 47 additions & 0 deletions
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+
raise ValueError(f"plugins cannot handle directive {directive}")
23+
return self._process_plugins(data)
24+
25+
def _process_plugins(self, data: Any) -> bool:
26+
success = True
27+
plugin_paths = []
28+
for item in data:
29+
self._log.lowinfo(f"Loading plugin from {item}")
30+
31+
plugin_path_globs = glob.glob(os.path.join(item, "*.py"))
32+
if not plugin_path_globs:
33+
success = False
34+
self._log.warning(f"Failed to load plugin from {item}")
35+
else:
36+
for plugin_path in plugin_path_globs:
37+
plugin_paths.append(plugin_path)
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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
9+
import dotbot
10+
11+
12+
class ConfigFile(dotbot.Plugin):
13+
def can_handle(self, directive):
14+
return directive == "plugin_config_file"
15+
16+
def handle(self, directive, data):
17+
with open(os.path.abspath(os.path.expanduser("~/flag")), "w") as file:
18+
file.write("config file plugin loading works")
19+
return True

0 commit comments

Comments
 (0)