401. Plugin infrastructure

* implements installing, loading and setup for plugins
* codifies configuration
* has a sample plugin
* docs
* tests
This commit is contained in:
Will Kahn-Greene
2012-03-12 21:17:08 -04:00
parent f10c3bb8e5
commit 29b6f91740
11 changed files with 541 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ from mediagoblin.tools.response import render_404
from mediagoblin.tools import request as mg_request
from mediagoblin.mg_globals import setup_globals
from mediagoblin.init.celery import setup_celery_from_config
from mediagoblin.init.plugins import setup_plugins
from mediagoblin.init import (get_jinja_loader, get_staticdirector,
setup_global_and_app_config, setup_workbench, setup_database,
setup_storage, setup_beaker_cache)
@@ -64,6 +65,11 @@ class MediaGoblinApp(object):
# Setup other connections / useful objects
##########################################
# Set up plugins -- need to do this early so that plugins can
# affect startup.
_log.info("Setting up plugins.")
setup_plugins()
# Set up the database
self.connection, self.db = setup_database()

View File

@@ -0,0 +1,59 @@
# 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 import mg_globals
from mediagoblin.tools import pluginapi
_log = logging.getLogger(__name__)
def setup_plugins():
"""This loads, configures and registers plugins
See plugin documentation for more details.
"""
global_config = mg_globals.global_config
plugin_section = global_config.get('plugins', {})
if not plugin_section:
_log.info("No plugins to load")
return
pcache = pluginapi.PluginCache()
# Go through and import all the modules that are subsections of
# the [plugins] section.
for plugin_module, config in plugin_section.items():
_log.info("Importing plugin module: %s" % 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)

View File

@@ -0,0 +1,6 @@
========
README
========
This directory holds the MediaGoblin core plugins. These plugins are not
enabled by default. See documentation for enabling plugins.

View File

@@ -0,0 +1,16 @@
# 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/>.

View File

@@ -0,0 +1,6 @@
========
README
========
This is a sample plugin. It does nothing interesting other than show
one way to structure a MediaGoblin plugin.

View File

@@ -0,0 +1,20 @@
# 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/>.
# 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

View File

@@ -0,0 +1,42 @@
# 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

View File

@@ -0,0 +1,158 @@
# 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 sys
from configobj import ConfigObj
from mediagoblin import mg_globals
from mediagoblin.init.plugins import setup_plugins
from mediagoblin.tools import pluginapi
from nose.tools import eq_
def with_cleanup(*modules_to_delete):
def _with_cleanup(fun):
"""Wrapper that saves and restores mg_globals"""
def _with_cleanup_inner(*args, **kwargs):
old_app_config = mg_globals.app_config
old_global_config = mg_globals.global_config
# Need to delete icky modules before and after so as to make
# sure things work correctly.
for module in modules_to_delete:
try:
del sys.modules[module]
except KeyError:
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()
try:
return fun(*args, **kwargs)
finally:
mg_globals.app_config = old_app_config
mg_globals.global_config = old_global_config
# Need to delete icky modules before and after so as to make
# sure things work correctly.
for module in modules_to_delete:
try:
del sys.modules[module]
except KeyError:
pass
pcache.clear()
_with_cleanup_inner.__name__ = fun.__name__
return _with_cleanup_inner
return _with_cleanup
def build_config(sections):
"""Builds a ConfigObj object with specified data
:arg sections: list of ``(section_name, section_data,
subsection_list)`` tuples where section_data is a dict and
subsection_list is a list of ``(section_name, section_data,
subsection_list)``, ...
For example:
>>> build_config([
... ('mediagoblin', {'key1': 'val1'}, []),
... ('section2', {}, [
... ('subsection1', {}, [])
... ])
... ])
"""
cfg = ConfigObj()
cfg.filename = 'foo'
def _iter_section(cfg, section_list):
for section_name, data, subsection_list in section_list:
cfg[section_name] = data
_iter_section(cfg[section_name], subsection_list)
_iter_section(cfg, sections)
return cfg
@with_cleanup()
def test_no_plugins():
"""Run setup_plugins with no plugins in config"""
cfg = build_config([('mediagoblin', {}, [])])
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
pcache = pluginapi.PluginCache()
setup_plugins()
# Make sure we didn't load anything.
eq_(len(pcache.plugin_classes), 0)
eq_(len(pcache.plugin_objects), 0)
@with_cleanup('mediagoblin.plugins.sampleplugin',
'mediagoblin.plugins.sampleplugin.main')
def test_one_plugin():
"""Run setup_plugins with a single working plugin"""
cfg = build_config([
('mediagoblin', {}, []),
('plugins', {}, [
('mediagoblin.plugins.sampleplugin', {}, [])
])
])
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
pcache = pluginapi.PluginCache()
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)
@with_cleanup('mediagoblin.plugins.sampleplugin',
'mediagoblin.plugins.sampleplugin.main')
def test_same_plugin_twice():
"""Run setup_plugins with a single working plugin twice"""
cfg = build_config([
('mediagoblin', {}, []),
('plugins', {}, [
('mediagoblin.plugins.sampleplugin', {}, []),
('mediagoblin.plugins.sampleplugin', {}, []),
])
])
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
pcache = pluginapi.PluginCache()
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)

View File

@@ -0,0 +1,118 @@
# 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/>.
"""
This module implements the plugin api bits and provides the plugin
base.
Two things about things in this module:
1. they should be excessively well documented because we should pull
from this file for the docs
2. they should be well tested
How do plugins work?
====================
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.
Lifecycle
=========
1. All the modules listed as subsections of the ``plugins`` section in
the config file are imported and any ``Plugin`` subclasses are
loaded causing it to be registered with the ``PluginCache``.
2. After all plugin modules are imported, registered plugins are
instantiated and ``setup_plugin`` is called with the configuration.
How to build a plugin
=====================
See the documentation on building plugins.
"""
import logging
from mediagoblin import mg_globals
_log = logging.getLogger(__name__)
class PluginCache(object):
"""Cache of plugin things"""
__state = {
# list of plugin classes
"plugin_classes": [],
# list of plugin objects
"plugin_objects": []
}
def clear(self):
"""This is only useful for testing."""
del self.plugin_classes[:]
del self.plugin_objects[:]
def __init__(self):
self.__dict__ = self.__state
def register_plugin_class(self, plugin_class):
"""Registers a plugin class"""
self.plugin_classes.append(plugin_class)
def register_plugin_object(self, plugin_obj):
"""Registers a plugin object"""
self.plugin_objects.append(plugin_obj)
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):
__metaclass__ = MetaPluginClass
def setup_plugin(self):
pass
def get_config(key):
"""Retrieves the configuration for a specified plugin by key
Example:
>>> get_config('mediagoblin.plugins.sampleplugin')
{'foo': 'bar'}
"""
global_config = mg_globals.global_config
plugin_section = global_config.get('plugins', {})
return plugin_section.get(key, {})