1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
18 This module implements the plugin api bits.
20 Two things about things in this module:
22 1. they should be excessively well documented because we should pull
23 from this file for the docs
25 2. they should be well tested
31 Plugins are structured like any Python project. You create a Python package.
32 In that package, you define a high-level ``__init__.py`` module that has a
33 ``hooks`` dict that maps hooks to callables that implement those hooks.
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::
40 |- setup.py # plugin project packaging metadata
41 |- README # holds plugin project information
42 |- LICENSE # holds license information
43 |- myplugin/ # plugin package directory
44 |- __init__.py # has hooks dict and code
50 1. All the modules listed as subsections of the ``plugins`` section in
51 the config file are imported. MediaGoblin registers any hooks in
52 the ``hooks`` dict of those modules.
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.
61 from functools
import wraps
63 from mediagoblin
import mg_globals
66 _log
= logging
.getLogger(__name__
)
69 class PluginManager(object):
70 """Manager for plugin things
74 This is a Borg class--there is one and only one of this class.
77 # list of plugin classes
80 # map of hook names -> list of callables for that hook
83 # list of registered template paths
84 "template_paths": set(),
86 # list of template hooks
89 # list of registered routes
94 """This is only useful for testing."""
95 # Why lists don't have a clear is not clear.
99 self
.template_paths
.clear()
102 self
.__dict
__ = self
.__state
104 def register_plugin(self
, plugin
):
105 """Registers a plugin class"""
106 self
.plugins
.append(plugin
)
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
))
114 # In this case, it's actually a single callable---not a
116 self
.hooks
.setdefault(hook
, []).append(callables
)
118 def get_hook_callables(self
, hook_name
):
119 return self
.hooks
.get(hook_name
, [])
121 def register_template_path(self
, path
):
122 """Registers a template path"""
123 self
.template_paths
.add(path
)
125 def get_template_paths(self
):
126 """Returns a tuple of registered template paths"""
127 return tuple(self
.template_paths
)
129 def register_route(self
, route
):
130 """Registers a single route"""
131 _log
.debug('registering route: {0}'.format(route
))
132 self
.routes
.append(route
)
134 def get_routes(self
):
135 return tuple(self
.routes
)
137 def register_template_hooks(self
, template_hooks
):
138 for hook
, templates
in template_hooks
.items():
139 if isinstance(templates
, (list, tuple)):
140 self
.template_hooks
.setdefault(hook
, []).extend(list(templates
))
142 # In this case, it's actually a single callable---not a
144 self
.template_hooks
.setdefault(hook
, []).append(templates
)
146 def get_template_hooks(self
, hook_name
):
147 return self
.template_hooks
.get(hook_name
, [])
150 def register_routes(routes
):
151 """Registers one or more routes
153 If your plugin handles requests, then you need to call this with
154 the routes your plugin handles.
156 A "route" is a `routes.Route` object. See `the routes.Route
158 <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for
161 Example passing in a single route:
163 >>> register_routes(('about-view', '/about',
164 ... 'mediagoblin.views:about_view_handler'))
166 Example passing in a list of routes:
168 >>> register_routes([
169 ... ('contact-view', '/contact', 'mediagoblin.views:contact_handler'),
170 ... ('about-view', '/about', 'mediagoblin.views:about_handler')
176 Be careful when designing your route urls. If they clash with
177 core urls, then it could result in DISASTER!
179 if isinstance(routes
, list):
181 PluginManager().register_route(route
)
183 PluginManager().register_route(routes
)
186 def register_template_path(path
):
187 """Registers a path for template loading
189 If your plugin has templates, then you need to call this with
190 the absolute path of the root of templates directory.
194 >>> my_plugin_dir = os.path.dirname(__file__)
195 >>> template_dir = os.path.join(my_plugin_dir, 'templates')
196 >>> register_template_path(template_dir)
200 You can only do this in `setup_plugins()`. Doing this after
201 that will have no effect on template loading.
204 PluginManager().register_template_path(path
)
208 """Retrieves the configuration for a specified plugin by key
212 >>> get_config('mediagoblin.plugins.sampleplugin')
214 >>> get_config('myplugin')
216 >>> get_config('flatpages')
217 {'directory': '/srv/mediagoblin/pages', 'nesting': 1}}
221 global_config
= mg_globals
.global_config
222 plugin_section
= global_config
.get('plugins', {})
223 return plugin_section
.get(key
, {})
226 def register_template_hooks(template_hooks
):
228 Register a dict of template hooks.
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
237 .. code-block:: python
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"]}
243 PluginManager().register_template_hooks(template_hooks
)
246 def get_hook_templates(hook_name
):
248 Get a list of hook templates for this hook_name.
250 Note: for the most part, you access this via a template tag, not
251 this method directly, like so:
253 .. code-block:: html+jinja
255 {% template_hook "media_sidebar" %}
257 ... which will include all templates for you, partly using this
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:
263 .. code-block:: html+jinja
265 {% for template_path in get_hook_templates("media_sidebar") %}
266 <div class="extra_structure">
267 {% include template_path %}
272 A list of strings representing template paths.
274 return PluginManager().get_template_hooks(hook_name
)
277 ###########################
278 # Callable convenience code
279 ###########################
281 class CantHandleIt(Exception):
283 A callable may call this method if they look at the relevant
284 arguments passed and decide it's not possible for them to handle
289 class UnhandledCallable(Exception):
291 Raise this method if no callables were available to handle the
292 specified hook. Only used by callable_runone.
297 def callable_runone(hookname
, unhandled_okay
=False, *args
, **kwargs
):
299 Run the callable hook HOOKNAME... run until the first response,
302 This function will run stop at the first hook that handles the
303 result. Hooks raising CantHandleIt will be skipped.
305 Unless unhandled_okay is True, this will error out if no hooks
306 have been registered to handle this function.
308 callables
= PluginManager().get_hook_callables(hookname
)
310 for callable in callables
:
312 return callable(*args
, **kwargs
)
316 if unhandled_okay
is False:
317 raise UnhandledCallable(
318 "No hooks registered capable of handling '%s'" % hookname
)
321 def callable_runall(hookname
, *args
, **kwargs
):
323 Run all callables for HOOKNAME.
325 This method will run *all* hooks that handle this method (skipping
326 those that raise CantHandleIt), and will return a list of all
329 callables
= PluginManager().get_hook_callables(hookname
)
333 for callable in callables
:
335 results
.append(callable(*args
, **kwargs
))