Monday, July 23, 2007

An Architecture For Plugin Code in Python

I recently needed a framework that would allow a user to create a Python module of functions that could be accessed by custom scripts launched from a different module entirely. I think I've written similar code three or four times now, so it's about time I came to grips with the "pattern" involved. Like most things in Python, it's dead simple once you get your head around it.

Our example plugin module we'll call plugin.py and make as simple as possible:
def Iron():
print 'Iron'

def Gold():
print metal

def Again():
Transform()
print 'Plastic'

I note that the second function appears to be referencing a variable metal that does not exist, and the third calls a function Transform that also seems to be missing in action. These are to be provided by the calling programme.

That module is named master.py and looks like this:

import os, sys

class Alchemy(object):
def __init__(self, filespec):
# add module to system path
filepath, filename = os.path.split(filespec)
if filepath not in sys.path:
sys.path.append(filepath)

# import module into a custom namespace
filename = os.path.splitext(filename)[0]
space = __import__(filename, globals(), locals(), [])
self.namespace = space.__dict__

def __call__(self, code):
# define a new function
def _Transform():
print ':: into ::'

# add that to our namespace along with a constant
self.namespace.update({'Transform': _Transform,
'metal': 'Gold'})

# ok, go for it!
exec code in self.namespace

myAlchemy = Alchemy('/path/to/plugin/plugin.py')
myAlchemy('Iron()\nTransform()\nGold()\nAgain()')

(To get this to work on your system you'll need to update the path in the second-last line.)

Run master.py and you should see five lines of output to the console.

I wrote this as a class to emphasise the difference between the setup work done in the init method and the execution work done in the call. The setup is likely done when our application launches, while the rendering of the code might occur many times during execution.

The importing uses the hook documented here. A namespace is just a dictionary mapping names to objects, instances, functions and so on. We can update this with whatever specific elements we require.

The example function here is trivial. In a more practical example _Transform() might (for example) provide important environment information to the plugin.

The nice thing about this architecture is that the plugin modules themselves need not be littered with all sorts of imports accessing internals of the application. Just remember to document the available functions for the plugin author.

Code similar to this provides the functionality of the new Wasp plugin system.

Maybe something like this will make it into the third edition of that invaluable reference, the Python Cookbook.

RELATED POSTS

No comments:

Post a comment