your plugin's code.
The ``__init__.py`` denotes that this is a Python package. It also
-holds the plugin code.
+holds the plugin code and the ``hooks`` dict that specifies which
+hooks the sampleplugin uses.
Step 2: README
.. code-block:: python
:linenos:
- :emphasize-lines: 8,19
+ :emphasize-lines: 12,23
import logging
from mediagoblin.tools.pluginapi import Plugin, get_config
+ # This creates a logger that you can use to log information to
+ # the console or a log file.
_log = logging.getLogger(__name__)
- class SamplePlugin(Plugin):
- """
- This is a sample plugin class. It automatically registers itself
- with MediaGoblin when this module is imported.
+ # This is the function that gets called when the setup
+ # hook fires.
+ def setup_plugin():
+ _log.info("I've been started!")
+ config = get_config('sampleplugin')
+ if config:
+ _log.info('%r' % config)
+ else:
+ _log.info('There is no configuration set.')
- The setup_plugin method prints configuration for this plugin if
- there is any.
- """
- def __init__(self):
- pass
- def setup_plugin(self):
- _log.info("I've been started!")
- config = get_config('sampleplugin')
- if config:
- _log.info('%r' % config)
- else:
- _log.info('There is no configuration set.')
+ # This is a dict that specifies which hooks this plugin uses.
+ # This one only uses one hook: setup.
+ hooks = {
+ 'setup': setup_plugin
+ }
-Line 8 defines a class called ``SamplePlugin`` that subclasses
-``Plugin`` from ``mediagoblin.tools.pluginapi``. When the class is
-defined, it gets registered with MediaGoblin and MediaGoblin will then
-call ``setup_plugin`` on it.
+Line 12 defines the ``setup_plugin`` function.
-Line 19 defines ``setup_plugin``. This gets called when MediaGoblin
-starts up after it's registered all the plugins. This is where you can
-do any initialization for your plugin.
+Line 23 defines ``hooks``. When MediaGoblin loads this file, it sees
+``hooks`` and registers all the callables with their respective hooks.
Step 6: Installation and configuration
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
setup_global_and_app_config, setup_workbench, setup_database,
setup_storage, setup_beaker_cache)
-from mediagoblin.tools.pluginapi import PluginCache
+from mediagoblin.tools.pluginapi import PluginManager
_log = logging.getLogger(__name__)
self.template_loader = get_jinja_loader(
app_config.get('local_templates'),
self.current_theme,
- PluginCache().get_template_paths()
+ PluginManager().get_template_paths()
)
# Set up storage systems
self.public_store, self.queue_store = setup_storage()
# set up routing
- self.routing = routing.get_mapper(PluginCache().get_routes())
+ self.routing = routing.get_mapper(PluginManager().get_routes())
# set up staticdirector tool
self.staticdirector = get_staticdirector(app_config)
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
+import sys
from mediagoblin import mg_globals
from mediagoblin.tools import pluginapi
_log.info("No plugins to load")
return
- pcache = pluginapi.PluginCache()
+ pman = pluginapi.PluginManager()
# Go through and import all the modules that are subsections of
- # the [plugins] section.
+ # the [plugins] section and read in the hooks.
for plugin_module, config in plugin_section.items():
_log.info("Importing plugin module: %s" % plugin_module)
+ pman.register_plugin(plugin_module)
# If this throws errors, that's ok--it'll halt mediagoblin
# startup.
__import__(plugin_module)
-
- # Note: One side-effect of importing things is that anything that
- # subclassed pluginapi.Plugin is registered.
-
- # Go through all the plugin classes, instantiate them, and call
- # setup_plugin so they can figure things out.
- for plugin_class in pcache.plugin_classes:
- name = plugin_class.__module__ + "." + plugin_class.__name__
- _log.info("Loading plugin: %s" % name)
- plugin_obj = plugin_class()
- plugin_obj.setup_plugin()
- pcache.register_plugin_object(plugin_obj)
+ plugin = sys.modules[plugin_module]
+ if hasattr(plugin, 'hooks'):
+ pman.register_hooks(plugin.hooks)
+
+ # Execute anything registered to the setup hook.
+ setup_list = pman.get_hook_callables('setup')
+ for fun in setup_list:
+ fun()
return _flatpage_handler_builder
-class FlatpagesFilePlugin(pluginapi.Plugin):
- """
- This is the flatpages plugin class. See the README for how to use
- flatpages.
- """
- def setup_plugin(self):
- self.config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile')
+def setup_plugin():
+ config = pluginapi.get_config('mediagoblin.plugins.flatpagesfile')
+
+ _log.info('Setting up flatpagesfile....')
+
+ # Register the template path.
+ pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
- _log.info('Setting up flatpagesfile....')
+ pages = config.items()
- # Register the template path.
- pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
+ routes = []
+ for name, (url, template) in pages:
+ name = 'flatpagesfile.%s' % name.strip()
+ controller = flatpage_handler_builder(template)
+ routes.append(
+ Route(name, url, controller=controller))
- pages = self.config.items()
+ pluginapi.register_routes(routes)
+ _log.info('Done setting up flatpagesfile!')
- routes = []
- for name, (url, template) in pages:
- name = 'flatpagesfile.%s' % name.strip()
- controller = flatpage_handler_builder(template)
- routes.append(
- Route(name, url, controller=controller))
- pluginapi.register_routes(routes)
- _log.info('Done setting up flatpagesfile!')
+hooks = {
+ 'setup': setup_plugin
+ }
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# This imports the module that has the Plugin subclass in it which
-# causes that module to get imported and that class to get registered.
-import mediagoblin.plugins.sampleplugin.main
+import logging
+
+from mediagoblin.tools.pluginapi import get_config
+
+
+_log = logging.getLogger(__name__)
+
+
+_setup_plugin_called = 0
+
+def setup_plugin():
+ global _setup_plugin_called
+
+ _log.info('Sample plugin set up!')
+ config = get_config('mediagoblin.plugins.sampleplugin')
+ if config:
+ _log.info('%r' % config)
+ else:
+ _log.info('There is no configuration set.')
+ _setup_plugin_called += 1
+
+
+hooks = {
+ 'setup': setup_plugin
+ }
+++ /dev/null
-# GNU MediaGoblin -- federated, autonomous media hosting
-# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import logging
-from mediagoblin.tools.pluginapi import Plugin, get_config
-
-
-_log = logging.getLogger(__name__)
-
-
-class SamplePlugin(Plugin):
- """
- This is a sample plugin class. It automatically registers itself
- with mediagoblin when this module is imported.
-
- The setup_plugin method prints configuration for this plugin if
- it exists.
- """
- def __init__(self):
- self._setup_plugin_called = 0
-
- def setup_plugin(self):
- _log.info('Sample plugin set up!')
- config = get_config('mediagoblin.plugins.sampleplugin')
- if config:
- _log.info('%r' % config)
- else:
- _log.info('There is no configuration set.')
- self._setup_plugin_called += 1
pass
# The plugin cache gets populated as a side-effect of
# importing, so it's best to clear it before and after a test.
- pcache = pluginapi.PluginCache()
- pcache.clear()
+ pman = pluginapi.PluginManager()
+ pman.clear()
try:
return fun(*args, **kwargs)
finally:
del sys.modules[module]
except KeyError:
pass
- pcache.clear()
+ pman.clear()
_with_cleanup_inner.__name__ = fun.__name__
return _with_cleanup_inner
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
- pcache = pluginapi.PluginCache()
+ pman = pluginapi.PluginManager()
setup_plugins()
# Make sure we didn't load anything.
- eq_(len(pcache.plugin_classes), 0)
- eq_(len(pcache.plugin_objects), 0)
+ eq_(len(pman.plugins), 0)
-@with_cleanup('mediagoblin.plugins.sampleplugin',
- 'mediagoblin.plugins.sampleplugin.main')
+@with_cleanup('mediagoblin.plugins.sampleplugin')
def test_one_plugin():
"""Run setup_plugins with a single working plugin"""
cfg = build_config([
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
- pcache = pluginapi.PluginCache()
+ pman = pluginapi.PluginManager()
setup_plugins()
- # Make sure we only found one plugin class
- eq_(len(pcache.plugin_classes), 1)
- # Make sure the class is the one we think it is.
- eq_(pcache.plugin_classes[0].__name__, 'SamplePlugin')
+ # Make sure we only found one plugin
+ eq_(len(pman.plugins), 1)
+ # Make sure the plugin is the one we think it is.
+ eq_(pman.plugins[0], 'mediagoblin.plugins.sampleplugin')
+ # Make sure there was one hook registered
+ eq_(len(pman.hooks), 1)
+ # Make sure _setup_plugin_called was called once
+ import mediagoblin.plugins.sampleplugin
+ eq_(mediagoblin.plugins.sampleplugin._setup_plugin_called, 1)
- # Make sure there was one plugin created
- eq_(len(pcache.plugin_objects), 1)
- # Make sure we called setup_plugin on SamplePlugin
- eq_(pcache.plugin_objects[0]._setup_plugin_called, 1)
-
-@with_cleanup('mediagoblin.plugins.sampleplugin',
- 'mediagoblin.plugins.sampleplugin.main')
+@with_cleanup('mediagoblin.plugins.sampleplugin')
def test_same_plugin_twice():
"""Run setup_plugins with a single working plugin twice"""
cfg = build_config([
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
- pcache = pluginapi.PluginCache()
+ pman = pluginapi.PluginManager()
setup_plugins()
- # Make sure we only found one plugin class
- eq_(len(pcache.plugin_classes), 1)
- # Make sure the class is the one we think it is.
- eq_(pcache.plugin_classes[0].__name__, 'SamplePlugin')
-
- # Make sure there was one plugin created
- eq_(len(pcache.plugin_objects), 1)
- # Make sure we called setup_plugin on SamplePlugin
- eq_(pcache.plugin_objects[0]._setup_plugin_called, 1)
+ # Make sure we only found one plugin
+ eq_(len(pman.plugins), 1)
+ # Make sure the plugin is the one we think it is.
+ eq_(pman.plugins[0], 'mediagoblin.plugins.sampleplugin')
+ # Make sure there was one hook registered
+ eq_(len(pman.hooks), 1)
+ # Make sure _setup_plugin_called was called once
+ import mediagoblin.plugins.sampleplugin
+ eq_(mediagoblin.plugins.sampleplugin._setup_plugin_called, 1)
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-This module implements the plugin api bits and provides the plugin
-base.
+This module implements the plugin api bits.
Two things about things in this module:
====================
Plugins are structured like any Python project. You create a Python package.
-In that package, you define a high-level ``__init__.py`` that either defines
-or imports modules that define classes that inherit from the ``Plugin`` class.
+In that package, you define a high-level ``__init__.py`` module that has a
+``hooks`` dict that maps hooks to callables that implement those hooks.
Additionally, you want a LICENSE file that specifies the license and a
``setup.py`` that specifies the metadata for packaging your plugin. A rough
|- README # holds plugin project information
|- LICENSE # holds license information
|- myplugin/ # plugin package directory
- |- __init__.py # imports myplugin.main
- |- main.py # code for plugin
+ |- __init__.py # has hooks dict and code
Lifecycle
=========
1. All the modules listed as subsections of the ``plugins`` section in
- the config file are imported. This causes any ``Plugin`` subclasses in
- those modules to be defined and when the classes are defined they get
- automatically registered with the ``PluginCache``.
+ the config file are imported. MediaGoblin registers any hooks in
+ the ``hooks`` dict of those modules.
-2. After all plugin modules are imported, registered plugin classes are
- instantiated and ``setup_plugin`` is called for each plugin object.
+2. After all plugin modules are imported, the ``setup`` hook is called
+ allowing plugins to do any set up they need to do.
- Plugins can do any setup they need to do in their ``setup_plugin``
- method.
"""
import logging
_log = logging.getLogger(__name__)
-class PluginCache(object):
- """Cache of plugin things"""
+class PluginManager(object):
+ """Manager for plugin things
+
+ .. Note::
+
+ This is a Borg class--there is one and only one of this class.
+ """
__state = {
# list of plugin classes
- "plugin_classes": [],
+ "plugins": [],
- # list of plugin objects
- "plugin_objects": [],
+ # map of hook names -> list of callables for that hook
+ "hooks": {},
# list of registered template paths
"template_paths": set(),
def clear(self):
"""This is only useful for testing."""
- del self.plugin_classes[:]
- del self.plugin_objects[:]
+ # Why lists don't have a clear is not clear.
+ del self.plugins[:]
+ del self.routes[:]
+ self.hooks.clear()
+ self.template_paths.clear()
def __init__(self):
self.__dict__ = self.__state
- def register_plugin_class(self, plugin_class):
+ def register_plugin(self, plugin):
"""Registers a plugin class"""
- self.plugin_classes.append(plugin_class)
+ self.plugins.append(plugin)
+
+ def register_hooks(self, hook_mapping):
+ """Takes a hook_mapping and registers all the hooks"""
+ for hook, callables in hook_mapping.items():
+ if isinstance(callables, (list, tuple)):
+ self.hooks.setdefault(hook, []).extend(list(callables))
+ else:
+ # In this case, it's actually a single callable---not a
+ # list of callables.
+ self.hooks.setdefault(hook, []).append(callables)
- def register_plugin_object(self, plugin_obj):
- """Registers a plugin object"""
- self.plugin_objects.append(plugin_obj)
+ def get_hook_callables(self, hook_name):
+ return self.hooks.get(hook_name, [])
def register_template_path(self, path):
"""Registers a template path"""
return tuple(self.routes)
-class MetaPluginClass(type):
- """Metaclass for PluginBase derivatives"""
- def __new__(cls, name, bases, attrs):
- new_class = super(MetaPluginClass, cls).__new__(cls, name, bases, attrs)
- parents = [b for b in bases if isinstance(b, MetaPluginClass)]
- if not parents:
- return new_class
- PluginCache().register_plugin_class(new_class)
- return new_class
-
-
-class Plugin(object):
- """Extend this class for plugins.
-
- Example::
-
- from mediagoblin.tools.pluginapi import Plugin
-
- class MyPlugin(Plugin):
- ...
-
- def setup_plugin(self):
- ....
-
- """
-
- __metaclass__ = MetaPluginClass
-
- def setup_plugin(self):
- pass
-
-
def register_routes(routes):
"""Registers one or more routes
"""
if isinstance(routes, (tuple, list)):
for route in routes:
- PluginCache().register_route(route)
+ PluginManager().register_route(route)
else:
- PluginCache().register_route(routes)
+ PluginManager().register_route(routes)
def register_template_path(path):
that will have no effect on template loading.
"""
- PluginCache().register_template_path(path)
+ PluginManager().register_template_path(path)
def get_config(key):