7c1e108b50b4ff7e67541a48cd3cca08bb0ea638
[mediagoblin.git] / mediagoblin / tools / pluginapi.py
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 registered routes
87 "routes": [],
88 }
89
90 def clear(self):
91 """This is only useful for testing."""
92 # Why lists don't have a clear is not clear.
93 del self.plugins[:]
94 del self.routes[:]
95 self.hooks.clear()
96 self.template_paths.clear()
97
98 def __init__(self):
99 self.__dict__ = self.__state
100
101 def register_plugin(self, plugin):
102 """Registers a plugin class"""
103 self.plugins.append(plugin)
104
105 def register_hooks(self, hook_mapping):
106 """Takes a hook_mapping and registers all the hooks"""
107 for hook, callables in hook_mapping.items():
108 if isinstance(callables, (list, tuple)):
109 self.hooks.setdefault(hook, []).extend(list(callables))
110 else:
111 # In this case, it's actually a single callable---not a
112 # list of callables.
113 self.hooks.setdefault(hook, []).append(callables)
114
115 def get_hook_callables(self, hook_name):
116 return self.hooks.get(hook_name, [])
117
118 def register_template_path(self, path):
119 """Registers a template path"""
120 self.template_paths.add(path)
121
122 def get_template_paths(self):
123 """Returns a tuple of registered template paths"""
124 return tuple(self.template_paths)
125
126 def register_route(self, route):
127 """Registers a single route"""
128 self.routes.append(route)
129
130 def get_routes(self):
131 return tuple(self.routes)
132
133
134 def register_routes(routes):
135 """Registers one or more routes
136
137 If your plugin handles requests, then you need to call this with
138 the routes your plugin handles.
139
140 A "route" is a `routes.Route` object. See `the routes.Route
141 documentation
142 <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for
143 more details.
144
145 Example passing in a single route:
146
147 >>> from routes import Route
148 >>> register_routes(Route('about-view', '/about',
149 ... controller=about_view_handler))
150
151 Example passing in a list of routes:
152
153 >>> from routes import Route
154 >>> register_routes([
155 ... Route('contact-view', '/contact', controller=contact_handler),
156 ... Route('about-view', '/about', controller=about_handler)
157 ... ])
158
159
160 .. Note::
161
162 Be careful when designing your route urls. If they clash with
163 core urls, then it could result in DISASTER!
164 """
165 if isinstance(routes, (tuple, list)):
166 for route in routes:
167 PluginManager().register_route(route)
168 else:
169 PluginManager().register_route(routes)
170
171
172 def register_template_path(path):
173 """Registers a path for template loading
174
175 If your plugin has templates, then you need to call this with
176 the absolute path of the root of templates directory.
177
178 Example:
179
180 >>> my_plugin_dir = os.path.dirname(__file__)
181 >>> template_dir = os.path.join(my_plugin_dir, 'templates')
182 >>> register_template_path(template_dir)
183
184 .. Note::
185
186 You can only do this in `setup_plugins()`. Doing this after
187 that will have no effect on template loading.
188
189 """
190 PluginManager().register_template_path(path)
191
192
193 def get_config(key):
194 """Retrieves the configuration for a specified plugin by key
195
196 Example:
197
198 >>> get_config('mediagoblin.plugins.sampleplugin')
199 {'foo': 'bar'}
200 >>> get_config('myplugin')
201 {}
202 >>> get_config('flatpages')
203 {'directory': '/srv/mediagoblin/pages', 'nesting': 1}}
204
205 """
206
207 global_config = mg_globals.global_config
208 plugin_section = global_config.get('plugins', {})
209 return plugin_section.get(key, {})
210
211