|
1 | 1 | # simplug |
2 | 2 |
|
3 | | -A simple plugin system for python with async hooks supported |
| 3 | +A simple plugin system for Python with async hooks support. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Decorator-based API**: Define hooks with `@simplug.spec` and implement with `@simplug.impl` |
| 8 | +- **Async support**: First-class async hooks with sync/async bridging |
| 9 | +- **Flexible result collection**: 18 built-in strategies for collecting plugin results |
| 10 | +- **Priority system**: Control plugin execution order |
| 11 | +- **Setuptools entrypoints**: Load plugins from installed packages |
| 12 | +- **Singleton per project**: Same instance returned for same project name |
4 | 13 |
|
5 | 14 | ## Installation |
6 | 15 |
|
7 | 16 | ```shell |
8 | 17 | pip install -U simplug |
9 | 18 | ``` |
10 | 19 |
|
11 | | -## Examples |
12 | | - |
13 | | -### A toy example |
| 20 | +## Quick Start |
14 | 21 |
|
15 | 22 | ```python |
16 | 23 | from simplug import Simplug |
17 | 24 |
|
18 | | -simplug = Simplug('project') |
| 25 | +# Create a plugin manager |
| 26 | +simplug = Simplug('myproject') |
19 | 27 |
|
| 28 | +# Define a hook specification |
20 | 29 | class MySpec: |
21 | | - """A hook specification namespace.""" |
22 | | - |
23 | 30 | @simplug.spec |
24 | | - def myhook(self, arg1, arg2): |
25 | | - """My special little hook that you can customize.""" |
26 | | - |
27 | | -class Plugin_1: |
28 | | - """A hook implementation namespace.""" |
| 31 | + def process_data(self, data): |
| 32 | + """Process data in plugins.""" |
29 | 33 |
|
| 34 | +# Implement the hook in plugins |
| 35 | +class PluginA: |
30 | 36 | @simplug.impl |
31 | | - def myhook(self, arg1, arg2): |
32 | | - print("inside Plugin_1.myhook()") |
33 | | - return arg1 + arg2 |
34 | | - |
35 | | -class Plugin_2: |
36 | | - """A 2nd hook implementation namespace.""" |
| 37 | + def process_data(self, data): |
| 38 | + return data.upper() |
37 | 39 |
|
| 40 | +class PluginB: |
38 | 41 | @simplug.impl |
39 | | - def myhook(self, arg1, arg2): |
40 | | - print("inside Plugin_2.myhook()") |
41 | | - return arg1 - arg2 |
42 | | - |
43 | | -simplug.register(Plugin_1, Plugin_2) |
44 | | -results = simplug.hooks.myhook(arg1=1, arg2=2) |
45 | | -print(results) |
46 | | -``` |
47 | | - |
48 | | -```shell |
49 | | -inside Plugin_1.myhook() |
50 | | -inside Plugin_2.myhook() |
51 | | -[3, -1] |
52 | | -``` |
53 | | - |
54 | | -Note that the hooks are executed in the order the plugins are registered. This is different from `pluggy`. |
55 | | - |
56 | | -### A complete example |
57 | | - |
58 | | -See `examples/complete/`. |
59 | | - |
60 | | -Running `python -m examples.complete` gets us: |
61 | | - |
62 | | -```shell |
63 | | -Your food. Enjoy some egg, egg, egg, salt, pepper, egg, egg |
64 | | -Some condiments? We have pickled walnuts, steak sauce, mushy peas, mint sauce |
65 | | -``` |
66 | | - |
67 | | -After install the plugin: |
68 | | - |
69 | | -```shell |
70 | | -> pip install --editable examples.complete.plugin |
71 | | -> python -m examples.complete # run again |
72 | | -``` |
73 | | - |
74 | | -```shell |
75 | | -Your food. Enjoy some egg, egg, egg, salt, pepper, egg, egg, lovely spam, wonderous spam |
76 | | -Some condiments? We have pickled walnuts, mushy peas, mint sauce, spam sauce |
77 | | -Now this is what I call a condiments tray! |
78 | | -``` |
79 | | - |
80 | | -## Usage |
81 | | - |
82 | | -### Definition of hooks |
83 | | - |
84 | | -Hooks are specified and implemented by decorating the functions with `simplug.spec` and `simplug.impl` respectively. |
| 42 | + def process_data(self, data): |
| 43 | + return data.lower() |
85 | 44 |
|
86 | | -`simplug` is initialized by: |
| 45 | +# Register plugins |
| 46 | +simplug.register(PluginA, PluginB) |
87 | 47 |
|
88 | | -```python |
89 | | -simplug = Simplug('project') |
90 | | -``` |
91 | | - |
92 | | -The `'project'` is a unique name to mark the project, which makes sure `Simplug('project')` get the same instance each time. |
93 | | - |
94 | | -Note that if `simplug` is initialized without `project`, then a name is generated automatically as such `project-0`, `project-1`, etc. |
95 | | - |
96 | | -Hook specification is marked by `simplug.spec`: |
97 | | - |
98 | | -```python |
99 | | -simplug = Simplug('project') |
100 | | - |
101 | | -@simplug.spec |
102 | | -def setup(args): |
103 | | - ... |
| 48 | +# Call the hook |
| 49 | +results = simplug.hooks.process_data("Hello") |
| 50 | +print(results) # ['HELLO', 'hello'] |
104 | 51 | ``` |
105 | 52 |
|
106 | | -`simplug.spec` can take some arguments: |
107 | | - |
108 | | -- `required`: Whether this hook is required to be implemented in plugins |
109 | | -- `result`: An enumerator to specify the way to collec the results. |
110 | | - - `SimplugResult.ALL`: Collect all results from all plugins |
111 | | - - `SimplugResult.ALL_AVAILS`: Get all the results from the hook, as a list, not including `None`s |
112 | | - - `SimplugResult.ALL_FIRST`: Executing all implementations and get the first result |
113 | | - - `SimplugResult.ALL_LAST`: Executing all implementations and get the last result |
114 | | - - `SimplugResult.TRY_ALL_FIRST`: Executing all implementations and get the first result, if no result is returned, return `None` |
115 | | - - `SimplugResult.TRY_ALL_LAST`: Executing all implementations and get the last result, if no result is returned, return `None` |
116 | | - - `SimplugResult.ALL_FIRST_AVAIL`: Executing all implementations and get the first non-`None` result |
117 | | - - `SimplugResult.ALL_LAST_AVAIL`: Executing all implementations and get the last non-`None` result |
118 | | - - `SimplugResult.TRY_ALL_FIRST_AVAIL`: Executing all implementations and get the first non-`None` result, if no result is returned, return `None` |
119 | | - - `SimplugResult.TRY_ALL_LAST_AVAIL`: Executing all implementations and get the last non-`None` result, if no result is returned, return `None` |
120 | | - - `SimplugResult.FIRST`: Get the first result, don't execute other implementations |
121 | | - - `SimplugResult.LAST`: Get the last result, don't execute other implementations |
122 | | - - `SimplugResult.TRY_FIRST`: Get the first result, don't execute other implementations, if no result is returned, return `None` |
123 | | - - `SimplugResult.TRY_LAST`: Get the last result, don't execute other implementations, if no result is returned, return `None` |
124 | | - - `SimplugResult.FIRST_AVAIL`: Get the first non-`None` result, don't execute other implementations |
125 | | - - `SimplugResult.LAST_AVAIL`: Get the last non-`None` result, don't execute other implementations |
126 | | - - `SimplugResult.TRY_FIRST_AVAIL`: Get the first non-`None` result, don't execute other implementations, if no result is returned, return `None` |
127 | | - - `SimplugResult.TRY_LAST_AVAIL`: Get the last non-`None` result, don't execute other implementations, if no result is returned, return `None` |
128 | | - - `SimplugResult.SINGLE`: Get the result from a single implementation |
129 | | - - `SimplugResult.TRY_SINGLE`: Get the result from a single implementation, if no result is returned, return `None` |
130 | | - - A callable to collect the result, take `calls` as the argument, a 3-element tuple with first element as the implementation, second element as the positional arguments, and third element as the keyword arguments. |
131 | | - |
132 | | -Hook implementation is marked by `simplug.impl`, which takes no additional arguments. |
133 | | - |
134 | | -The name of the function has to match the name of the function by `simplug.spec`. And the signatures of the specification function and the implementation function have to be the same in terms of names. This means you can specify default values in the specification function, but you don't have to write the default values in the implementation function. |
135 | | - |
136 | | -Note that default values in implementation functions will be ignored. |
137 | | - |
138 | | -Also note if a hook specification is under a namespace, it can take `self` as argument. However, this argument will be ignored while the hook is being called (`self` will be `None`, and you still have to specify it in the function definition). |
139 | | - |
140 | | -### Loading plugins from setuptools entrypoint |
141 | | - |
142 | | -You have to call `simplug.load_entrypoints(group)` after the hook specifications are defined to load the plugins registered by setuptools entrypoint. If `group` is not given, the project name will be used. |
| 53 | +## Documentation |
143 | 54 |
|
144 | | -### The plugin registry |
| 55 | +Full documentation is available at [https://pwwang.github.io/simplug/](https://pwwang.github.io/simplug/) |
145 | 56 |
|
146 | | -The plugins are registered by `simplug.register(*plugins)`. Each plugin of `plugins` can be either a python object or a str denoting a module that can be imported by `importlib.import_module`. |
| 57 | +## Getting Started |
147 | 58 |
|
148 | | -The python object must have an attribute `name`, `__name__` or `__class.__name__` for `simplug` to determine the name of the plugin. If the plugin name is determined from `__name__` or `__class__.__name__`, it will be lowercased. |
| 59 | +1. **Define hooks** - Specify what plugins can customize |
| 60 | +2. **Implement hooks** - Create plugins with implementations |
| 61 | +3. **Register plugins** - Load plugins into your application |
| 62 | +4. **Call hooks** - Trigger plugin execution |
149 | 63 |
|
150 | | -If a plugin is loaded from setuptools entrypoint, then the entrypoint name will be used (no matter what name is defined inside the plugin) |
| 64 | +For detailed guides and API reference, see the [full documentation](https://pwwang.github.io/simplug/). |
151 | 65 |
|
152 | | -You can enable or disable a plugin temporarily after registration by: |
153 | | - |
154 | | -```python |
155 | | -simplug.disable('plugin_name') |
156 | | -simplug.enable('plugin_name') |
157 | | -``` |
158 | | - |
159 | | -You can use following methods to inspect the plugin registry: |
160 | | - |
161 | | -- `simplug.get_plugin`: Get the plugin by name |
162 | | -- `simplug.get_all_plugins`: Get a dictionary of name-plugin mappings of all plugins |
163 | | -- `simplug.get_all_plugin_names`: Get the names of all plugins, in the order it will be executed. |
164 | | -- `simplug.get_enabled_plugins`: Get a dictionary of name-plugin mappings of all enabled plugins |
165 | | -- `simplug.get_enabled_plugin_names`: Get the names of all enabled plugins, in the order it will be executed. |
166 | | - |
167 | | -### Calling hooks |
168 | | - |
169 | | -Hooks are call by `simplug.hooks.<hook_name>(<arguments>)` and results are collected based on the `result` argument passed in `simplug.spec` when defining hooks. |
170 | | - |
171 | | -### Async hooks |
172 | | - |
173 | | -It makes no big difference to define an async hook: |
174 | | - |
175 | | -```python |
176 | | -@simplug.spec |
177 | | -async def async_hook(arg): |
178 | | - ... |
179 | | - |
180 | | -# to supress warnings for sync implementation |
181 | | -@simplug.spec(warn_sync_impl_on_async=False) |
182 | | -async def async_hook(arg): |
183 | | - ... |
184 | | -``` |
185 | | - |
186 | | -One can implement this hook in either an async or a sync way. However, when implementing it in a sync way, a warning will be raised. To suppress the warning, one can pass a `False` value of argument `warn_sync_impl_on_async` to `simplug.spec`. |
| 66 | +## Examples |
187 | 67 |
|
188 | | -To call the async hooks (`simplug.hooks.async_hook(arg)`), you will just need to call it like any other async functions (using `asyncio.run`, for example) |
| 68 | +See the [examples](examples/) directory for complete examples: |
| 69 | +- [`examples/toy.py`](examples/toy.py) - A minimal 30-line demo |
| 70 | +- [`examples/complete/`](examples/complete/) - Full example with setuptools entrypoints |
189 | 71 |
|
190 | | -## API |
| 72 | +## License |
191 | 73 |
|
192 | | -https://pwwang.github.io/simplug/ |
| 74 | +MIT |
0 commit comments