pluginapi.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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. This module implements the plugin api bits.
  18. Two things about things in this module:
  19. 1. they should be excessively well documented because we should pull
  20. from this file for the docs
  21. 2. they should be well tested
  22. How do plugins work?
  23. ====================
  24. Plugins are structured like any Python project. You create a Python package.
  25. In that package, you define a high-level ``__init__.py`` module that has a
  26. ``hooks`` dict that maps hooks to callables that implement those hooks.
  27. Additionally, you want a LICENSE file that specifies the license and a
  28. ``setup.py`` that specifies the metadata for packaging your plugin. A rough
  29. file structure could look like this::
  30. myplugin/
  31. |- setup.py # plugin project packaging metadata
  32. |- README # holds plugin project information
  33. |- LICENSE # holds license information
  34. |- myplugin/ # plugin package directory
  35. |- __init__.py # has hooks dict and code
  36. Lifecycle
  37. =========
  38. 1. All the modules listed as subsections of the ``plugins`` section in
  39. the config file are imported. MediaGoblin registers any hooks in
  40. the ``hooks`` dict of those modules.
  41. 2. After all plugin modules are imported, the ``setup`` hook is called
  42. allowing plugins to do any set up they need to do.
  43. """
  44. import logging
  45. from functools import wraps
  46. from mediagoblin import mg_globals
  47. _log = logging.getLogger(__name__)
  48. class PluginManager(object):
  49. """Manager for plugin things
  50. .. Note::
  51. This is a Borg class--there is one and only one of this class.
  52. """
  53. __state = {
  54. # list of plugin classes
  55. "plugins": [],
  56. # map of hook names -> list of callables for that hook
  57. "hooks": {},
  58. # list of registered template paths
  59. "template_paths": set(),
  60. # list of template hooks
  61. "template_hooks": {},
  62. # list of registered routes
  63. "routes": [],
  64. }
  65. def clear(self):
  66. """This is only useful for testing."""
  67. # Why lists don't have a clear is not clear.
  68. del self.plugins[:]
  69. del self.routes[:]
  70. self.hooks.clear()
  71. self.template_paths.clear()
  72. def __init__(self):
  73. self.__dict__ = self.__state
  74. def register_plugin(self, plugin):
  75. """Registers a plugin class"""
  76. self.plugins.append(plugin)
  77. def register_hooks(self, hook_mapping):
  78. """Takes a hook_mapping and registers all the hooks"""
  79. for hook, callables in hook_mapping.items():
  80. if isinstance(callables, (list, tuple)):
  81. self.hooks.setdefault(hook, []).extend(list(callables))
  82. else:
  83. # In this case, it's actually a single callable---not a
  84. # list of callables.
  85. self.hooks.setdefault(hook, []).append(callables)
  86. def get_hook_callables(self, hook_name):
  87. return self.hooks.get(hook_name, [])
  88. def register_template_path(self, path):
  89. """Registers a template path"""
  90. self.template_paths.add(path)
  91. def get_template_paths(self):
  92. """Returns a tuple of registered template paths"""
  93. return tuple(self.template_paths)
  94. def register_route(self, route):
  95. """Registers a single route"""
  96. _log.debug('registering route: {0}'.format(route))
  97. self.routes.append(route)
  98. def get_routes(self):
  99. return tuple(self.routes)
  100. def register_template_hooks(self, template_hooks):
  101. for hook, templates in template_hooks.items():
  102. if isinstance(templates, (list, tuple)):
  103. self.template_hooks.setdefault(hook, []).extend(list(templates))
  104. else:
  105. # In this case, it's actually a single callable---not a
  106. # list of callables.
  107. self.template_hooks.setdefault(hook, []).append(templates)
  108. def get_template_hooks(self, hook_name):
  109. return self.template_hooks.get(hook_name, [])
  110. def register_routes(routes):
  111. """Registers one or more routes
  112. If your plugin handles requests, then you need to call this with
  113. the routes your plugin handles.
  114. A "route" is a `routes.Route` object. See `the routes.Route
  115. documentation
  116. <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for
  117. more details.
  118. Example passing in a single route:
  119. >>> register_routes(('about-view', '/about',
  120. ... 'mediagoblin.views:about_view_handler'))
  121. Example passing in a list of routes:
  122. >>> register_routes([
  123. ... ('contact-view', '/contact', 'mediagoblin.views:contact_handler'),
  124. ... ('about-view', '/about', 'mediagoblin.views:about_handler')
  125. ... ])
  126. .. Note::
  127. Be careful when designing your route urls. If they clash with
  128. core urls, then it could result in DISASTER!
  129. """
  130. if isinstance(routes, list):
  131. for route in routes:
  132. PluginManager().register_route(route)
  133. else:
  134. PluginManager().register_route(routes)
  135. def register_template_path(path):
  136. """Registers a path for template loading
  137. If your plugin has templates, then you need to call this with
  138. the absolute path of the root of templates directory.
  139. Example:
  140. >>> my_plugin_dir = os.path.dirname(__file__)
  141. >>> template_dir = os.path.join(my_plugin_dir, 'templates')
  142. >>> register_template_path(template_dir)
  143. .. Note::
  144. You can only do this in `setup_plugins()`. Doing this after
  145. that will have no effect on template loading.
  146. """
  147. PluginManager().register_template_path(path)
  148. def get_config(key):
  149. """Retrieves the configuration for a specified plugin by key
  150. Example:
  151. >>> get_config('mediagoblin.plugins.sampleplugin')
  152. {'foo': 'bar'}
  153. >>> get_config('myplugin')
  154. {}
  155. >>> get_config('flatpages')
  156. {'directory': '/srv/mediagoblin/pages', 'nesting': 1}}
  157. """
  158. global_config = mg_globals.global_config
  159. plugin_section = global_config.get('plugins', {})
  160. return plugin_section.get(key, {})
  161. def register_template_hooks(template_hooks):
  162. """
  163. Register a dict of template hooks.
  164. Takes template_hooks as an argument, which is a dictionary of
  165. template hook names/keys to the templates they should provide.
  166. (The value can either be a single template path or an iterable
  167. of paths.)
  168. Example:
  169. .. code-block:: python
  170. {"media_sidebar": "/plugin/sidemess/mess_up_the_side.html",
  171. "media_descriptionbox": ["/plugin/sidemess/even_more_mess.html",
  172. "/plugin/sidemess/so_much_mess.html"]}
  173. """
  174. PluginManager().register_template_hooks(template_hooks)
  175. def get_hook_templates(hook_name):
  176. """
  177. Get a list of hook templates for this hook_name.
  178. Note: for the most part, you access this via a template tag, not
  179. this method directly, like so:
  180. .. code-block:: html+jinja
  181. {% template_hook("media_sidebar") %}
  182. ... which will include all templates for you, partly using this
  183. method.
  184. However, this method is exposed to templates, and if you wish, you
  185. can iterate over templates in a template hook manually like so:
  186. .. code-block:: html+jinja
  187. {% for template_path in get_hook_templates("media_sidebar") %}
  188. <div class="extra_structure">
  189. {% include template_path %}
  190. </div>
  191. {% endfor %}
  192. Returns:
  193. A list of strings representing template paths.
  194. """
  195. return PluginManager().get_template_hooks(hook_name)
  196. #############################
  197. ## Hooks: The Next Generation
  198. #############################
  199. def hook_handle(hook_name, *args, **kwargs):
  200. """
  201. Run through hooks attempting to find one that handle this hook.
  202. All callables called with the same arguments until one handles
  203. things and returns a non-None value.
  204. (If you are writing a handler and you don't have a particularly
  205. useful value to return even though you've handled this, returning
  206. True is a good solution.)
  207. Note that there is a special keyword argument:
  208. if "default_handler" is passed in as a keyword argument, this will
  209. be used if no handler is found.
  210. Some examples of using this:
  211. - You need an interface implemented, but only one fit for it
  212. - You need to *do* something, but only one thing needs to do it.
  213. """
  214. default_handler = kwargs.pop('default_handler', None)
  215. callables = PluginManager().get_hook_callables(hook_name)
  216. result = None
  217. for callable in callables:
  218. result = callable(*args, **kwargs)
  219. if result is not None:
  220. break
  221. if result is None and default_handler is not None:
  222. result = default_handler(*args, **kwargs)
  223. return result
  224. def hook_runall(hook_name, *args, **kwargs):
  225. """
  226. Run through all callable hooks and pass in arguments.
  227. All non-None results are accrued in a list and returned from this.
  228. (Other "false-like" values like False and friends are still
  229. accrued, however.)
  230. Some examples of using this:
  231. - You have an interface call where actually multiple things can
  232. and should implement it
  233. - You need to get a list of things from various plugins that
  234. handle them and do something with them
  235. - You need to *do* something, and actually multiple plugins need
  236. to do it separately
  237. """
  238. callables = PluginManager().get_hook_callables(hook_name)
  239. results = []
  240. for callable in callables:
  241. result = callable(*args, **kwargs)
  242. if result is not None:
  243. results.append(result)
  244. return results
  245. def hook_transform(hook_name, arg):
  246. """
  247. Run through a bunch of hook callables and transform some input.
  248. Note that unlike the other hook tools, this one only takes ONE
  249. argument. This argument is passed to each function, which in turn
  250. returns something that becomes the input of the next callable.
  251. Some examples of using this:
  252. - You have an object, say a form, but you want plugins to each be
  253. able to modify it.
  254. """
  255. result = arg
  256. callables = PluginManager().get_hook_callables(hook_name)
  257. for callable in callables:
  258. result = callable(result)
  259. return result