Skip to content

Instantly share code, notes, and snippets.

@felippemr
Forked from mepcotterell/LICENSE
Created April 19, 2016 23:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felippemr/a4ad54369600bb3f824e22cdd432b60e to your computer and use it in GitHub Desktop.
Save felippemr/a4ad54369600bb3f824e22cdd432b60e to your computer and use it in GitHub Desktop.
Simple Python Plugin Manager
def action_hello(hook_params):
print 'hello %s' % hook_params['name']
def filter_hello(hook_params):
return {'name': '%s johnson' % hook_params['name']}

Simple Python Plugin Manager

Note: In order for this example to work, you need place __init__.py into plugins/example/ where plugins.py, example.py and the plugins directory are in the same directory.

Note: This implementation requires Python 2.7 or higher.

Expected Output

$ python plugins.py
INFO:root:plugin "test" loaded
hello bob
{'name': 'bob johnson'}
INFO:root:plugin "test" unloaded

How To Use

Using the PluginManager class is easy:

import plugins

# create a plugin manager
plugin_manager = plugins.PluginManager('./plugins')

In the above example, we're saying that plugin modules are stored in the ./plugins directory.

Now, assume that you've placed the example __init__.py file in ./plugins/example/__init__.py. Then, in order to load the plugin, you use the plugin manager's load_plugin function:

plugin_manager.load_plugin('example')

Similarly, you can use the unload_plugin to unload the plugin if you so desire.

In the example __init__.py file, we're expecting to be able to extend the main application at two different hooks: action_hello and filter_hello (more on hooks below). This means that somewhere in your application, you need to use the plugin manager's execute_action_hook and execute_filter_hook functions:

# executes action_hello(hook_params) in ./plugins/*/__init__.py (if loaded)
plugin_manager.execute_action_hook('hello', {'name': 'bob'})

# executes filter_hello(hook_params) in ./plugins/*/__init__.py (if loaded)
result = plugin_manager.execute_filter_hook('hello', {'name': 'bob'})
print result

Hooks

Hooks are basically places in your application where you allow plugins to extend your application. This plugin manager supports two different kinds of hooks, action hooks and filter hooks. These hooks should be documented for your plugin developers.

Action Hooks

Action hooks can be thought of as places in your code where you would like plugins to be able to execute arbitrary code (perform actions), possibly using a dict that your application provides to them. In the example __init__.py file, an action hook called hello is registered by defining an action_hello function. The example application passed the following dictionary to all plugins that register the hook: {'name': 'bob'}.

Note: In order to ensure that actions occur in the order you need them to, the loaded plugins are executed in the same order in which they are loaded.

Filter Hooks

Filter hooks are places in you code where you would llike plugins to be able to take a dict, modifiy it, and return it. An interesting use case for this might be a plugin that replaces bbcode or smilies with HTML image tags. In the example __init__.py file, a filter hook called hello is registered by defining a filter_hello function.

Note, the PluginManager class checks that the dictionary returned by the plugins contains the same set of keys as when the execute_filter_hook function was called. For example, since the dictionary passed to the execute_filter_hook function in the example application contains a single key called name, then an exception would be raised if the filter_hello function in the example __init__.py file did not return a dictionary that key in it.

Note: In order to ensure that filtering of the dictionary occurs in the order you need it to, the loaded plugins are executed in the same order in which they are loaded.

Main Module

By default, this implementation expects that the main module for each plugin is __init__. You can change this by using the optional main_module parameter in the PluginManager constructor. For example, if you wanted plugin.py in each plugin folder to be a plugin's main module, then you could do the following:

plugin_manager = plugins.PluginManager('./plugins', 'plugin')

Logging

By defauly, this implementation uses it's own logger from logging. If you would like for the PluginManager class to you use your own logger, you can specify one using the optional log parameter in the PluginManager constructor.

import plugins
if __name__ == '__main__':
# create a plugin manager
plugin_manager = plugins.PluginManager('./plugins')
# load all plugin modules
for plugin in plugin_manager.get_available_plugins():
plugin_manager.load_plugin(plugin)
# executes action_hello(hook_params) in ./plugins/*/__init__.py (if loaded)
plugin_manager.execute_action_hook('hello', {'name': 'bob'})
# executes filter_hello(hook_params) in ./plugins/*/__init__.py (if loaded)
result = plugin_manager.execute_filter_hook('hello', {'name': 'bob'})
print result
# load all plugin modules
for plugin in plugin_manager.get_loaded_plugins():
plugin_manager.unload_plugin(plugin)
The MIT License (MIT)
Copyright (c) 2013 Michael E. Cotterell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
__author__ = "Michael E. Cotterell"
__email__ = "mepcotterell@gmail.com"
__copyright__ = "Copyright 2013, Michael E. Cotterell"
__license__ = "MIT"
import imp
import os
import logging
import collections
logging.basicConfig(level = logging.DEBUG)
class PluginManager:
'''
A simple plugin manager
'''
def __init__(self, plugin_folder, main_module = '__init__', log = logging):
self.logging = log
self.plugin_folder = plugin_folder
self.main_module = main_module
self.loaded_plugins = collections.OrderedDict({})
def get_available_plugins(self):
'''
Returns a dictionary of plugins available in the plugin folder
'''
plugins = {}
for possible in os.listdir(self.plugin_folder):
location = os.path.join(self.plugin_folder, possible)
if os.path.isdir(location) and self.main_module + '.py' in os.listdir(location):
info = imp.find_module(self.main_module, [location])
plugins[possible] = {
'name': possible,
'info': info
}
return plugins
def get_loaded_plugins(self):
'''
Returns a dictionary of the loaded plugin modules
'''
return self.loaded_plugins.copy()
def load_plugin(self, plugin_name):
'''
Loads a plugin module
'''
plugins = self.get_available_plugins()
if plugin_name in plugins:
if plugin_name not in self.loaded_plugins:
module = imp.load_module(self.main_module, *plugins[plugin_name]['info'])
self.loaded_plugins[plugin_name] = {
'name': plugin_name,
'info': plugins[plugin_name]['info'],
'module': module
}
self.logging.info('plugin "%s" loaded' % plugin_name)
else:
self.logging.warn('plugin "%s" already loaded' % plugin_name)
else:
self.logging.error('cannot locate plugin "%s"' % plugin_name)
raise Exception('cannot locate plugin "%s"' % plugin_name)
def unload_plugin(self, plugin_name):
'''
Unloads a plugin module
'''
del self.loaded_plugins[plugin_name]
self.logging.info('plugin "%s" unloaded' % plugin_name)
def execute_action_hook(self, hook_name, hook_params = {}):
'''
Executes action hook functions of the form action_hook_name contained in
the loaded plugin modules.
'''
for key, plugin_info in self.loaded_plugins.items():
module = plugin_info['module']
hook_func_name = 'action_%s' % hook_name
if hasattr(module, hook_func_name):
hook_func = getattr(module, hook_func_name)
hook_func(hook_params)
def execute_filter_hook(self, hook_name, hook_params = {}):
'''
Filters the hook_params through filter hook functions of the form
filter_hook_name contained in the loaded plugin modules.
'''
hook_params_keys = hook_params.keys()
for key, plugin_info in self.loaded_plugins.items():
module = plugin_info['module']
hook_func_name = 'filter_%s' % hook_name
if hasattr(module, hook_func_name):
hook_func = getattr(module, hook_func_name)
hook_params = hook_func(hook_params)
for nkey in hook_params_keys:
if nkey not in hook_params.keys():
msg = 'function "%s" in plugin "%s" is missing "%s" in the dict it returns' % (hook_func_name, plugin_info['name'], nkey)
self.logging.error(msg)
raise Exception(msg)
return hook_params
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment