| 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 | """ |
| 18 | This module implements the plugin api bits. |
| 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 | |
| 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. |
| 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 |
| 44 | |- __init__.py # has hooks dict and code |
| 45 | |
| 46 | |
| 47 | Lifecycle |
| 48 | ========= |
| 49 | |
| 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. |
| 53 | |
| 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. |
| 56 | |
| 57 | """ |
| 58 | |
| 59 | import logging |
| 60 | |
| 61 | from functools import wraps |
| 62 | |
| 63 | from mediagoblin import mg_globals |
| 64 | |
| 65 | |
| 66 | _log = logging.getLogger(__name__) |
| 67 | |
| 68 | |
| 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 | """ |
| 76 | __state = { |
| 77 | # list of plugin classes |
| 78 | "plugins": [], |
| 79 | |
| 80 | # map of hook names -> list of callables for that hook |
| 81 | "hooks": {}, |
| 82 | |
| 83 | # list of registered template paths |
| 84 | "template_paths": set(), |
| 85 | |
| 86 | # list of template hooks |
| 87 | "template_hooks": {}, |
| 88 | |
| 89 | # list of registered routes |
| 90 | "routes": [], |
| 91 | } |
| 92 | |
| 93 | def clear(self): |
| 94 | """This is only useful for testing.""" |
| 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() |
| 100 | |
| 101 | def __init__(self): |
| 102 | self.__dict__ = self.__state |
| 103 | |
| 104 | def register_plugin(self, plugin): |
| 105 | """Registers a plugin class""" |
| 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) |
| 117 | |
| 118 | def get_hook_callables(self, hook_name): |
| 119 | return self.hooks.get(hook_name, []) |
| 120 | |
| 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 | |
| 129 | def register_route(self, route): |
| 130 | """Registers a single route""" |
| 131 | _log.debug('registering route: {0}'.format(route)) |
| 132 | self.routes.append(route) |
| 133 | |
| 134 | def get_routes(self): |
| 135 | return tuple(self.routes) |
| 136 | |
| 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)) |
| 141 | else: |
| 142 | # In this case, it's actually a single callable---not a |
| 143 | # list of callables. |
| 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, []) |
| 148 | |
| 149 | |
| 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 | |
| 163 | >>> register_routes(('about-view', '/about', |
| 164 | ... 'mediagoblin.views:about_view_handler')) |
| 165 | |
| 166 | Example passing in a list of routes: |
| 167 | |
| 168 | >>> register_routes([ |
| 169 | ... ('contact-view', '/contact', 'mediagoblin.views:contact_handler'), |
| 170 | ... ('about-view', '/about', 'mediagoblin.views:about_handler') |
| 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 | """ |
| 179 | if isinstance(routes, list): |
| 180 | for route in routes: |
| 181 | PluginManager().register_route(route) |
| 182 | else: |
| 183 | PluginManager().register_route(routes) |
| 184 | |
| 185 | |
| 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 | """ |
| 204 | PluginManager().register_template_path(path) |
| 205 | |
| 206 | |
| 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'} |
| 214 | >>> get_config('myplugin') |
| 215 | {} |
| 216 | >>> get_config('flatpages') |
| 217 | {'directory': '/srv/mediagoblin/pages', 'nesting': 1}} |
| 218 | |
| 219 | """ |
| 220 | |
| 221 | global_config = mg_globals.global_config |
| 222 | plugin_section = global_config.get('plugins', {}) |
| 223 | return plugin_section.get(key, {}) |
| 224 | |
| 225 | |
| 226 | def register_template_hooks(template_hooks): |
| 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: |
| 236 | |
| 237 | .. code-block:: python |
| 238 | |
| 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 | """ |
| 243 | PluginManager().register_template_hooks(template_hooks) |
| 244 | |
| 245 | |
| 246 | def get_hook_templates(hook_name): |
| 247 | """ |
| 248 | Get a list of hook templates for this hook_name. |
| 249 | |
| 250 | Note: for the most part, you access this via a template tag, not |
| 251 | this method directly, like so: |
| 252 | |
| 253 | .. code-block:: html+jinja |
| 254 | |
| 255 | {% template_hook("media_sidebar") %} |
| 256 | |
| 257 | ... which will include all templates for you, partly using this |
| 258 | method. |
| 259 | |
| 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 | |
| 263 | .. code-block:: html+jinja |
| 264 | |
| 265 | {% for template_path in get_hook_templates("media_sidebar") %} |
| 266 | <div class="extra_structure"> |
| 267 | {% include template_path %} |
| 268 | </div> |
| 269 | {% endfor %} |
| 270 | |
| 271 | Returns: |
| 272 | A list of strings representing template paths. |
| 273 | """ |
| 274 | return PluginManager().get_template_hooks(hook_name) |
| 275 | |
| 276 | |
| 277 | ############################# |
| 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 | """ |
| 301 | default_handler = kwargs.pop('default_handler', None) |
| 302 | |
| 303 | callables = PluginManager().get_hook_callables(hook_name) |
| 304 | |
| 305 | result = None |
| 306 | |
| 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 | |
| 316 | return result |
| 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 |