Merge remote-tracking branch 'refs/remotes/willkg/plugins-infrastructure-rewrite'

This commit is contained in:
Christopher Allan Webber
2012-07-26 10:46:52 -05:00
9 changed files with 207 additions and 196 deletions

View File

@@ -32,7 +32,7 @@ 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)
from mediagoblin.tools.pluginapi import PluginCache
from mediagoblin.tools.pluginapi import PluginManager
_log = logging.getLogger(__name__)
@@ -82,14 +82,14 @@ class MediaGoblinApp(object):
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)

View File

@@ -15,6 +15,7 @@
# 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
@@ -36,24 +37,28 @@ def setup_plugins():
_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():
# Skip any modules that start with -. This makes it easier for
# someone to tweak their configuration so as to not load a
# plugin without having to remove swaths of plugin
# configuration.
if plugin_module.startswith('-'):
continue
_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)
plugin = sys.modules[plugin_module]
if hasattr(plugin, 'hooks'):
pman.register_hooks(plugin.hooks)
# 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)
# Execute anything registered to the setup hook.
setup_list = pman.get_hook_callables('setup')
for fun in setup_list:
fun()

View File

@@ -53,27 +53,27 @@ def flatpage_handler_builder(template):
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....')
_log.info('Setting up flatpagesfile....')
# Register the template path.
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
# Register the template path.
pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
pages = self.config.items()
pages = config.items()
routes = []
for name, (url, template) in pages:
name = 'flatpagesfile.%s' % name.strip()
controller = flatpage_handler_builder(template)
routes.append(
Route(name, url, controller=controller))
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!')
pluginapi.register_routes(routes)
_log.info('Done setting up flatpagesfile!')
hooks = {
'setup': setup_plugin
}

View File

@@ -15,6 +15,28 @@
# 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
}

View File

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

@@ -37,8 +37,8 @@ def with_cleanup(*modules_to_delete):
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:
@@ -51,7 +51,7 @@ def with_cleanup(*modules_to_delete):
del sys.modules[module]
except KeyError:
pass
pcache.clear()
pman.clear()
_with_cleanup_inner.__name__ = fun.__name__
return _with_cleanup_inner
@@ -93,16 +93,14 @@ def test_no_plugins():
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([
@@ -115,22 +113,21 @@ def test_one_plugin():
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)
@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([
@@ -144,15 +141,35 @@ def test_same_plugin_twice():
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()
def test_disabled_plugin():
"""Run setup_plugins with a single working plugin twice"""
cfg = build_config([
('mediagoblin', {}, []),
('plugins', {}, [
('-mediagoblin.plugins.sampleplugin', {}, []),
])
])
mg_globals.app_config = cfg['mediagoblin']
mg_globals.global_config = cfg
pman = pluginapi.PluginManager()
setup_plugins()
# Make sure we didn't load the plugin
eq_(len(pman.plugins), 0)

View File

@@ -15,8 +15,7 @@
# 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:
@@ -30,8 +29,8 @@ How do plugins work?
====================
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
@@ -42,23 +41,19 @@ file structure could look like this::
|- 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
@@ -69,14 +64,19 @@ from mediagoblin import mg_globals
_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(),
@@ -87,19 +87,31 @@ class PluginCache(object):
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_plugin_object(self, plugin_obj):
"""Registers a plugin object"""
self.plugin_objects.append(plugin_obj)
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 get_hook_callables(self, hook_name):
return self.hooks.get(hook_name, [])
def register_template_path(self, path):
"""Registers a template path"""
@@ -117,38 +129,6 @@ class PluginCache(object):
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
@@ -182,9 +162,9 @@ def register_routes(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):
@@ -205,7 +185,7 @@ 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):