Add image URL's (thumb & full)
[mediagoblin.git] / mediagoblin / tools / pluginapi.py
CommitLineData
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 18This module implements the plugin api bits.
29b6f917
WKG
19
20Two things about things in this module:
21
221. they should be excessively well documented because we should pull
23 from this file for the docs
24
252. they should be well tested
26
27
28How do plugins work?
29====================
30
355fd677 31Plugins are structured like any Python project. You create a Python package.
f46e2a4d 32In 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
35Additionally, you want a LICENSE file that specifies the license and a
36``setup.py`` that specifies the metadata for packaging your plugin. A rough
37file 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
47Lifecycle
48=========
49
501. 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
542. 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
59import logging
60
f46e2a4d
JW
61from functools import wraps
62
29b6f917
WKG
63from mediagoblin import mg_globals
64
65
66_log = logging.getLogger(__name__)
67
68
05e007c1
WKG
69class 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
150def 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
186def 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
207def 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 226def 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
246def 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
282def 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
319def 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
348def 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