Commit | Line | Data |
---|---|---|
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 | 18 | This module implements the plugin api bits. |
29b6f917 WKG |
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 | ||
355fd677 | 31 | Plugins are structured like any Python project. You create a Python package. |
f46e2a4d | 32 | In 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 | |
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 | |
05e007c1 | 44 | |- __init__.py # has hooks dict and code |
29b6f917 WKG |
45 | |
46 | ||
47 | Lifecycle | |
48 | ========= | |
49 | ||
50 | 1. 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 |
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. | |
29b6f917 | 56 | |
29b6f917 WKG |
57 | """ |
58 | ||
59 | import logging | |
60 | ||
f46e2a4d JW |
61 | from functools import wraps |
62 | ||
29b6f917 WKG |
63 | from mediagoblin import mg_globals |
64 | ||
65 | ||
66 | _log = logging.getLogger(__name__) | |
67 | ||
68 | ||
05e007c1 WKG |
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 | """ | |
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 WKG |
85 | |
86 | # list of registered routes | |
87 | "routes": [], | |
29b6f917 WKG |
88 | } |
89 | ||
90 | def clear(self): | |
91 | """This is only useful for testing.""" | |
05e007c1 WKG |
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() | |
29b6f917 WKG |
97 | |
98 | def __init__(self): | |
99 | self.__dict__ = self.__state | |
100 | ||
05e007c1 | 101 | def register_plugin(self, plugin): |
29b6f917 | 102 | """Registers a plugin class""" |
05e007c1 WKG |
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) | |
29b6f917 | 114 | |
05e007c1 WKG |
115 | def get_hook_callables(self, hook_name): |
116 | return self.hooks.get(hook_name, []) | |
29b6f917 | 117 | |
8545dd50 WKG |
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 | ||
4bd65f69 WKG |
126 | def register_route(self, route): |
127 | """Registers a single route""" | |
d56e8263 | 128 | _log.debug('registering route: {0}'.format(route)) |
4bd65f69 WKG |
129 | self.routes.append(route) |
130 | ||
131 | def get_routes(self): | |
132 | return tuple(self.routes) | |
133 | ||
29b6f917 | 134 | |
4bd65f69 WKG |
135 | def register_routes(routes): |
136 | """Registers one or more routes | |
137 | ||
138 | If your plugin handles requests, then you need to call this with | |
139 | the routes your plugin handles. | |
140 | ||
141 | A "route" is a `routes.Route` object. See `the routes.Route | |
142 | documentation | |
143 | <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for | |
144 | more details. | |
145 | ||
146 | Example passing in a single route: | |
147 | ||
60389b21 SS |
148 | >>> register_routes(('about-view', '/about', |
149 | ... 'mediagoblin.views:about_view_handler')) | |
4bd65f69 WKG |
150 | |
151 | Example passing in a list of routes: | |
152 | ||
4bd65f69 | 153 | >>> register_routes([ |
60389b21 SS |
154 | ... ('contact-view', '/contact', 'mediagoblin.views:contact_handler'), |
155 | ... ('about-view', '/about', 'mediagoblin.views:about_handler') | |
4bd65f69 WKG |
156 | ... ]) |
157 | ||
158 | ||
159 | .. Note:: | |
160 | ||
161 | Be careful when designing your route urls. If they clash with | |
162 | core urls, then it could result in DISASTER! | |
163 | """ | |
7742dcc1 | 164 | if isinstance(routes, list): |
4bd65f69 | 165 | for route in routes: |
05e007c1 | 166 | PluginManager().register_route(route) |
4bd65f69 | 167 | else: |
05e007c1 | 168 | PluginManager().register_route(routes) |
4bd65f69 WKG |
169 | |
170 | ||
8545dd50 WKG |
171 | def register_template_path(path): |
172 | """Registers a path for template loading | |
173 | ||
174 | If your plugin has templates, then you need to call this with | |
175 | the absolute path of the root of templates directory. | |
176 | ||
177 | Example: | |
178 | ||
179 | >>> my_plugin_dir = os.path.dirname(__file__) | |
180 | >>> template_dir = os.path.join(my_plugin_dir, 'templates') | |
181 | >>> register_template_path(template_dir) | |
182 | ||
183 | .. Note:: | |
184 | ||
185 | You can only do this in `setup_plugins()`. Doing this after | |
186 | that will have no effect on template loading. | |
187 | ||
188 | """ | |
05e007c1 | 189 | PluginManager().register_template_path(path) |
8545dd50 WKG |
190 | |
191 | ||
29b6f917 WKG |
192 | def get_config(key): |
193 | """Retrieves the configuration for a specified plugin by key | |
194 | ||
195 | Example: | |
196 | ||
197 | >>> get_config('mediagoblin.plugins.sampleplugin') | |
198 | {'foo': 'bar'} | |
355fd677 WKG |
199 | >>> get_config('myplugin') |
200 | {} | |
201 | >>> get_config('flatpages') | |
202 | {'directory': '/srv/mediagoblin/pages', 'nesting': 1}} | |
203 | ||
29b6f917 WKG |
204 | """ |
205 | ||
206 | global_config = mg_globals.global_config | |
207 | plugin_section = global_config.get('plugins', {}) | |
208 | return plugin_section.get(key, {}) | |
f46e2a4d JW |
209 | |
210 |