Finish flatpagesplugin; add plugin docs
[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 and provides the plugin
19 base.
20
21 Two things about things in this module:
22
23 1. they should be excessively well documented because we should pull
24 from this file for the docs
25
26 2. they should be well tested
27
28
29 How do plugins work?
30 ====================
31
32 Plugins are structured like any Python project. You create a Python package.
33 In that package, you define a high-level ``__init__.py`` that either defines
34 or imports modules that define classes that inherit from the ``Plugin`` class.
35
36 Additionally, you want a LICENSE file that specifies the license and a
37 ``setup.py`` that specifies the metadata for packaging your plugin. A rough
38 file structure could look like this::
39
40 myplugin/
41 |- setup.py # plugin project packaging metadata
42 |- README # holds plugin project information
43 |- LICENSE # holds license information
44 |- myplugin/ # plugin package directory
45 |- __init__.py # imports myplugin.main
46 |- main.py # code for plugin
47
48
49 Lifecycle
50 =========
51
52 1. All the modules listed as subsections of the ``plugins`` section in
53 the config file are imported. This causes any ``Plugin`` subclasses in
54 those modules to be defined and when the classes are defined they get
55 automatically registered with the ``PluginCache``.
56
57 2. After all plugin modules are imported, registered plugin classes are
58 instantiated and ``setup_plugin`` is called for each plugin object.
59
60 Plugins can do any setup they need to do in their ``setup_plugin``
61 method.
62 """
63
64 import logging
65
66 from mediagoblin import mg_globals
67
68
69 _log = logging.getLogger(__name__)
70
71
72 class PluginCache(object):
73 """Cache of plugin things"""
74 __state = {
75 # list of plugin classes
76 "plugin_classes": [],
77
78 # list of plugin objects
79 "plugin_objects": [],
80
81 # list of registered template paths
82 "template_paths": set(),
83
84 # list of registered routes
85 "routes": [],
86 }
87
88 def clear(self):
89 """This is only useful for testing."""
90 del self.plugin_classes[:]
91 del self.plugin_objects[:]
92
93 def __init__(self):
94 self.__dict__ = self.__state
95
96 def register_plugin_class(self, plugin_class):
97 """Registers a plugin class"""
98 self.plugin_classes.append(plugin_class)
99
100 def register_plugin_object(self, plugin_obj):
101 """Registers a plugin object"""
102 self.plugin_objects.append(plugin_obj)
103
104 def register_template_path(self, path):
105 """Registers a template path"""
106 self.template_paths.add(path)
107
108 def get_template_paths(self):
109 """Returns a tuple of registered template paths"""
110 return tuple(self.template_paths)
111
112 def register_route(self, route):
113 """Registers a single route"""
114 self.routes.append(route)
115
116 def get_routes(self):
117 return tuple(self.routes)
118
119
120 class MetaPluginClass(type):
121 """Metaclass for PluginBase derivatives"""
122 def __new__(cls, name, bases, attrs):
123 new_class = super(MetaPluginClass, cls).__new__(cls, name, bases, attrs)
124 parents = [b for b in bases if isinstance(b, MetaPluginClass)]
125 if not parents:
126 return new_class
127 PluginCache().register_plugin_class(new_class)
128 return new_class
129
130
131 class Plugin(object):
132 """Extend this class for plugins.
133
134 Example::
135
136 from mediagoblin.tools.pluginapi import Plugin
137
138 class MyPlugin(Plugin):
139 ...
140
141 def setup_plugin(self):
142 ....
143
144 """
145
146 __metaclass__ = MetaPluginClass
147
148 def setup_plugin(self):
149 pass
150
151
152 def register_routes(routes):
153 """Registers one or more routes
154
155 If your plugin handles requests, then you need to call this with
156 the routes your plugin handles.
157
158 A "route" is a `routes.Route` object. See `the routes.Route
159 documentation
160 <http://routes.readthedocs.org/en/latest/modules/route.html>`_ for
161 more details.
162
163 Example passing in a single route:
164
165 >>> from routes import Route
166 >>> register_routes(Route('about-view', '/about',
167 ... controller=about_view_handler))
168
169 Example passing in a list of routes:
170
171 >>> from routes import Route
172 >>> register_routes([
173 ... Route('contact-view', '/contact', controller=contact_handler),
174 ... Route('about-view', '/about', controller=about_handler)
175 ... ])
176
177
178 .. Note::
179
180 Be careful when designing your route urls. If they clash with
181 core urls, then it could result in DISASTER!
182 """
183 if isinstance(routes, (tuple, list)):
184 for route in routes:
185 PluginCache().register_route(route)
186 else:
187 PluginCache().register_route(routes)
188
189
190 def register_template_path(path):
191 """Registers a path for template loading
192
193 If your plugin has templates, then you need to call this with
194 the absolute path of the root of templates directory.
195
196 Example:
197
198 >>> my_plugin_dir = os.path.dirname(__file__)
199 >>> template_dir = os.path.join(my_plugin_dir, 'templates')
200 >>> register_template_path(template_dir)
201
202 .. Note::
203
204 You can only do this in `setup_plugins()`. Doing this after
205 that will have no effect on template loading.
206
207 """
208 PluginCache().register_template_path(path)
209
210
211 def get_config(key):
212 """Retrieves the configuration for a specified plugin by key
213
214 Example:
215
216 >>> get_config('mediagoblin.plugins.sampleplugin')
217 {'foo': 'bar'}
218 >>> get_config('myplugin')
219 {}
220 >>> get_config('flatpages')
221 {'directory': '/srv/mediagoblin/pages', 'nesting': 1}}
222
223 """
224
225 global_config = mg_globals.global_config
226 plugin_section = global_config.get('plugins', {})
227 return plugin_section.get(key, {})