Merge branch '512_bump_video_js'
[mediagoblin.git] / mediagoblin / tools / pluginapi.py
CommitLineData
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 18This module implements the plugin api bits.
29b6f917
WKG
19
20Two things about things in this module:
21
221. they should be excessively well documented because we should pull
23 from this file for the docs
24
252. they should be well tested
26
27
28How do plugins work?
29====================
30
355fd677 31Plugins are structured like any Python project. You create a Python package.
f46e2a4d 32In 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
35Additionally, you want a LICENSE file that specifies the license and a
36``setup.py`` that specifies the metadata for packaging your plugin. A rough
37file 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
47Lifecycle
48=========
49
501. 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
542. 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
59import logging
60
f46e2a4d
JW
61from functools import wraps
62
29b6f917
WKG
63from mediagoblin import mg_globals
64
65
66_log = logging.getLogger(__name__)
67
68
05e007c1
WKG
69class 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
135def 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
171def 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
192def 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