Commit | Line | Data |
---|---|---|
29b6f917 WKG |
1 | # GNU MediaGoblin -- federated, autonomous media hosting |
2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU Affero General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | """ | |
05e007c1 | 18 | This module implements the plugin api bits. |
29b6f917 WKG |
19 | |
20 | Two things about things in this module: | |
21 | ||
22 | 1. they should be excessively well documented because we should pull | |
23 | from this file for the docs | |
24 | ||
25 | 2. they should be well tested | |
26 | ||
27 | ||
28 | How do plugins work? | |
29 | ==================== | |
30 | ||
355fd677 | 31 | Plugins are structured like any Python project. You create a Python package. |
f46e2a4d | 32 | In that package, you define a high-level ``__init__.py`` module that has a |
05e007c1 | 33 | ``hooks`` dict that maps hooks to callables that implement those hooks. |
355fd677 WKG |
34 | |
35 | Additionally, you want a LICENSE file that specifies the license and a | |
36 | ``setup.py`` that specifies the metadata for packaging your plugin. A rough | |
37 | file structure could look like this:: | |
38 | ||
39 | myplugin/ | |
40 | |- setup.py # plugin project packaging metadata | |
41 | |- README # holds plugin project information | |
42 | |- LICENSE # holds license information | |
43 | |- myplugin/ # plugin package directory | |
05e007c1 | 44 | |- __init__.py # has hooks dict and code |
29b6f917 WKG |
45 | |
46 | ||
47 | Lifecycle | |
48 | ========= | |
49 | ||
50 | 1. All the modules listed as subsections of the ``plugins`` section in | |
05e007c1 WKG |
51 | the config file are imported. MediaGoblin registers any hooks in |
52 | the ``hooks`` dict of those modules. | |
29b6f917 | 53 | |
05e007c1 WKG |
54 | 2. After all plugin modules are imported, the ``setup`` hook is called |
55 | allowing plugins to do any set up they need to do. | |
29b6f917 | 56 | |
29b6f917 WKG |
57 | """ |
58 | ||
59 | import logging | |
60 | ||
f46e2a4d JW |
61 | from functools import wraps |
62 | ||
29b6f917 WKG |
63 | from mediagoblin import mg_globals |
64 | ||
65 | ||
66 | _log = logging.getLogger(__name__) | |
67 | ||
68 | ||
05e007c1 WKG |
69 | class PluginManager(object): |
70 | """Manager for plugin things | |
71 | ||
72 | .. Note:: | |
73 | ||
74 | This is a Borg class--there is one and only one of this class. | |
75 | """ | |
29b6f917 WKG |
76 | __state = { |
77 | # list of plugin classes | |
05e007c1 | 78 | "plugins": [], |
29b6f917 | 79 | |
05e007c1 WKG |
80 | # map of hook names -> list of callables for that hook |
81 | "hooks": {}, | |
8545dd50 WKG |
82 | |
83 | # list of registered template paths | |
84 | "template_paths": set(), | |
4bd65f69 | 85 | |
1c2d01ae CAW |
86 | # list of template hooks |
87 | "template_hooks": {}, | |
88 | ||
4bd65f69 WKG |
89 | # list of registered routes |
90 | "routes": [], | |
29b6f917 WKG |
91 | } |
92 | ||
93 | def clear(self): | |
94 | """This is only useful for testing.""" | |
05e007c1 WKG |
95 | # Why lists don't have a clear is not clear. |
96 | del self.plugins[:] | |
97 | del self.routes[:] | |
98 | self.hooks.clear() | |
99 | self.template_paths.clear() | |
29b6f917 WKG |
100 | |
101 | def __init__(self): | |
102 | self.__dict__ = self.__state | |
103 | ||
05e007c1 | 104 | def register_plugin(self, plugin): |
29b6f917 | 105 | """Registers a plugin class""" |
05e007c1 WKG |
106 | self.plugins.append(plugin) |
107 | ||
108 | def register_hooks(self, hook_mapping): | |
109 | """Takes a hook_mapping and registers all the hooks""" | |
110 | for hook, callables in hook_mapping.items(): | |
111 | if isinstance(callables, (list, tuple)): | |
112 | self.hooks.setdefault(hook, []).extend(list(callables)) | |
113 | else: | |
114 | # In this case, it's actually a single callable---not a | |
115 | # list of callables. | |
116 | self.hooks.setdefault(hook, []).append(callables) | |
29b6f917 | 117 | |
05e007c1 WKG |
118 | def get_hook_callables(self, hook_name): |
119 | return self.hooks.get(hook_name, []) | |
29b6f917 | 120 | |
8545dd50 WKG |
121 | def register_template_path(self, path): |
122 | """Registers a template path""" | |
123 | self.template_paths.add(path) | |
124 | ||
125 | def get_template_paths(self): | |
126 | """Returns a tuple of registered template paths""" | |
127 | return tuple(self.template_paths) | |
128 | ||
4bd65f69 WKG |
129 | def register_route(self, route): |
130 | """Registers a single route""" | |
d56e8263 | 131 | _log.debug('registering route: {0}'.format(route)) |
4bd65f69 WKG |
132 | self.routes.append(route) |
133 | ||
134 | def get_routes(self): | |
135 | return tuple(self.routes) | |
136 | ||
1c2d01ae | 137 | def register_template_hooks(self, template_hooks): |
1c2d01ae CAW |
138 | for hook, templates in template_hooks.items(): |
139 | if isinstance(templates, (list, tuple)): | |
140 | self.template_hooks.setdefault(hook, []).extend(list(templates)) | |
141 | else: | |
142 | # In this case, it's actually a single callable---not a | |
143 | # list of callables. | |
a3f811a6 CAW |
144 | self.template_hooks.setdefault(hook, []).append(templates) |
145 | ||
146 | def get_template_hooks(self, hook_name): | |
147 | return self.template_hooks.get(hook_name, []) | |
1c2d01ae | 148 | |
29b6f917 | 149 | |
4bd65f69 WKG |
150 | def register_routes(routes): |
151 | """Registers one or more routes | |
152 | ||
153 | If your plugin handles requests, then you need to call this with | |
154 | the routes your plugin handles. | |
155 | ||
156 | A "route" is a `routes.Route` object. See `the routes.Route | |
157 | documentation | |
158 | <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for | |
159 | more details. | |
160 | ||
161 | Example passing in a single route: | |
162 | ||
60389b21 SS |
163 | >>> register_routes(('about-view', '/about', |
164 | ... 'mediagoblin.views:about_view_handler')) | |
4bd65f69 WKG |
165 | |
166 | Example passing in a list of routes: | |
167 | ||
4bd65f69 | 168 | >>> register_routes([ |
60389b21 SS |
169 | ... ('contact-view', '/contact', 'mediagoblin.views:contact_handler'), |
170 | ... ('about-view', '/about', 'mediagoblin.views:about_handler') | |
4bd65f69 WKG |
171 | ... ]) |
172 | ||
173 | ||
174 | .. Note:: | |
175 | ||
176 | Be careful when designing your route urls. If they clash with | |
177 | core urls, then it could result in DISASTER! | |
178 | """ | |
7742dcc1 | 179 | if isinstance(routes, list): |
4bd65f69 | 180 | for route in routes: |
05e007c1 | 181 | PluginManager().register_route(route) |
4bd65f69 | 182 | else: |
05e007c1 | 183 | PluginManager().register_route(routes) |
4bd65f69 WKG |
184 | |
185 | ||
8545dd50 WKG |
186 | def register_template_path(path): |
187 | """Registers a path for template loading | |
188 | ||
189 | If your plugin has templates, then you need to call this with | |
190 | the absolute path of the root of templates directory. | |
191 | ||
192 | Example: | |
193 | ||
194 | >>> my_plugin_dir = os.path.dirname(__file__) | |
195 | >>> template_dir = os.path.join(my_plugin_dir, 'templates') | |
196 | >>> register_template_path(template_dir) | |
197 | ||
198 | .. Note:: | |
199 | ||
200 | You can only do this in `setup_plugins()`. Doing this after | |
201 | that will have no effect on template loading. | |
202 | ||
203 | """ | |
05e007c1 | 204 | PluginManager().register_template_path(path) |
8545dd50 WKG |
205 | |
206 | ||
29b6f917 WKG |
207 | def get_config(key): |
208 | """Retrieves the configuration for a specified plugin by key | |
209 | ||
210 | Example: | |
211 | ||
212 | >>> get_config('mediagoblin.plugins.sampleplugin') | |
213 | {'foo': 'bar'} | |
355fd677 WKG |
214 | >>> get_config('myplugin') |
215 | {} | |
216 | >>> get_config('flatpages') | |
217 | {'directory': '/srv/mediagoblin/pages', 'nesting': 1}} | |
218 | ||
29b6f917 WKG |
219 | """ |
220 | ||
221 | global_config = mg_globals.global_config | |
222 | plugin_section = global_config.get('plugins', {}) | |
223 | return plugin_section.get(key, {}) | |
f46e2a4d JW |
224 | |
225 | ||
1c2d01ae | 226 | def register_template_hooks(template_hooks): |
08f3966d CAW |
227 | """ |
228 | Register a dict of template hooks. | |
229 | ||
230 | Takes template_hooks as an argument, which is a dictionary of | |
231 | template hook names/keys to the templates they should provide. | |
232 | (The value can either be a single template path or an iterable | |
233 | of paths.) | |
234 | ||
235 | Example: | |
cf41e7d7 E |
236 | |
237 | .. code-block:: python | |
238 | ||
08f3966d CAW |
239 | {"media_sidebar": "/plugin/sidemess/mess_up_the_side.html", |
240 | "media_descriptionbox": ["/plugin/sidemess/even_more_mess.html", | |
241 | "/plugin/sidemess/so_much_mess.html"]} | |
242 | """ | |
1c2d01ae | 243 | PluginManager().register_template_hooks(template_hooks) |
a3f811a6 CAW |
244 | |
245 | ||
246 | def get_hook_templates(hook_name): | |
08f3966d CAW |
247 | """ |
248 | Get a list of hook templates for this hook_name. | |
249 | ||
f097cf64 CAW |
250 | Note: for the most part, you access this via a template tag, not |
251 | this method directly, like so: | |
252 | ||
cf41e7d7 E |
253 | .. code-block:: html+jinja |
254 | ||
cc0c6cd2 | 255 | {% template_hook("media_sidebar") %} |
f097cf64 CAW |
256 | |
257 | ... which will include all templates for you, partly using this | |
258 | method. | |
259 | ||
ec553298 CAW |
260 | However, this method is exposed to templates, and if you wish, you |
261 | can iterate over templates in a template hook manually like so: | |
262 | ||
cf41e7d7 E |
263 | .. code-block:: html+jinja |
264 | ||
ec553298 CAW |
265 | {% for template_path in get_hook_templates("media_sidebar") %} |
266 | <div class="extra_structure"> | |
cf41e7d7 | 267 | {% include template_path %} |
ec553298 CAW |
268 | </div> |
269 | {% endfor %} | |
270 | ||
08f3966d CAW |
271 | Returns: |
272 | A list of strings representing template paths. | |
273 | """ | |
a3f811a6 | 274 | return PluginManager().get_template_hooks(hook_name) |
e495e28e CAW |
275 | |
276 | ||
c5d8d301 | 277 | ############################# |
ff259f6b CAW |
278 | ## Hooks: The Next Generation |
279 | ############################# | |
280 | ||
281 | ||
282 | def hook_handle(hook_name, *args, **kwargs): | |
283 | """ | |
284 | Run through hooks attempting to find one that handle this hook. | |
285 | ||
286 | All callables called with the same arguments until one handles | |
287 | things and returns a non-None value. | |
288 | ||
289 | (If you are writing a handler and you don't have a particularly | |
290 | useful value to return even though you've handled this, returning | |
291 | True is a good solution.) | |
292 | ||
293 | Note that there is a special keyword argument: | |
294 | if "default_handler" is passed in as a keyword argument, this will | |
295 | be used if no handler is found. | |
296 | ||
297 | Some examples of using this: | |
298 | - You need an interface implemented, but only one fit for it | |
299 | - You need to *do* something, but only one thing needs to do it. | |
300 | """ | |
e49b7bf2 | 301 | default_handler = kwargs.pop('default_handler', None) |
ff259f6b CAW |
302 | |
303 | callables = PluginManager().get_hook_callables(hook_name) | |
304 | ||
eff722ef CAW |
305 | result = None |
306 | ||
ff259f6b CAW |
307 | for callable in callables: |
308 | result = callable(*args, **kwargs) | |
309 | ||
310 | if result is not None: | |
311 | break | |
312 | ||
313 | if result is None and default_handler is not None: | |
314 | result = default_handler(*args, **kwargs) | |
315 | ||
234ddad6 | 316 | return result |
ff259f6b CAW |
317 | |
318 | ||
319 | def hook_runall(hook_name, *args, **kwargs): | |
320 | """ | |
321 | Run through all callable hooks and pass in arguments. | |
322 | ||
323 | All non-None results are accrued in a list and returned from this. | |
324 | (Other "false-like" values like False and friends are still | |
325 | accrued, however.) | |
326 | ||
327 | Some examples of using this: | |
328 | - You have an interface call where actually multiple things can | |
329 | and should implement it | |
330 | - You need to get a list of things from various plugins that | |
331 | handle them and do something with them | |
332 | - You need to *do* something, and actually multiple plugins need | |
333 | to do it separately | |
334 | """ | |
335 | callables = PluginManager().get_hook_callables(hook_name) | |
336 | ||
337 | results = [] | |
338 | ||
339 | for callable in callables: | |
340 | result = callable(*args, **kwargs) | |
341 | ||
342 | if result is not None: | |
343 | results.append(result) | |
344 | ||
345 | return results | |
346 | ||
347 | ||
348 | def hook_transform(hook_name, arg): | |
349 | """ | |
350 | Run through a bunch of hook callables and transform some input. | |
351 | ||
352 | Note that unlike the other hook tools, this one only takes ONE | |
353 | argument. This argument is passed to each function, which in turn | |
354 | returns something that becomes the input of the next callable. | |
355 | ||
356 | Some examples of using this: | |
357 | - You have an object, say a form, but you want plugins to each be | |
358 | able to modify it. | |
359 | """ | |
360 | result = arg | |
361 | ||
362 | callables = PluginManager().get_hook_callables(hook_name) | |
363 | ||
364 | for callable in callables: | |
365 | result = callable(result) | |
366 | ||
367 | return result |