While working on the initial version of DjangoSVN, I realised that one of the coolest things I could do with the DjangoSVN command interpreter was to make it pluggable.

The idea was simple: You can set up a DjangoSVN server anywhere, then write your own custom plug-ins that you can just drop into the plug-in folder AND make use of the Django ORM should you want to give it database backing and an admin screen.

So I had to figure out how to make that work, and it was surprisingly easy, so this will be a short post. Credit where it is due, I found a lot of help over at StackOverflow – so never underestimate the true power of Google :-)

So, how do you write a plug-in framework?

The first step is planning – in order to use a plug-in, it should have a set, fixed format that can be written in. In the case of DjangoSVN, I wrote a ‘master’ plug-in class that could be inherited from any new plug-in to add functionality – this would ensure that the interface between your app and any new plug-in will always work the same way. It also gives you a chance to think about how you’ll pass values to it and return them in a standardised way.

Here’s the interface for DjangoSVN plug-ins:

class dsvn_hook:
    """
        Base SVN Hook class, svncommand will load a hook class from the
        target command and pass it the args and dsvncommand() object.
    """



    def __init__(self, args=None, dsvnobj=None):
        self.args = args # The command arguments that are after the command
        self.dsvnobj = dsvnobj # the dsvncommand() object that is called by the parser
       
        # Can be ignored, but if set in init() will have an effect on how repository
        # paths are managed.
       
        self.django = False
   
    def execute_hook(self):
        """
            This is what gets called by the command parser - override this function to
            execute complex scripts
        """

       
        None

It’s pretty straightforward – essentially you have a class, with an init() function you can override. You can put whatever you like in here that’s specific to your app.

It get’s interesting when you look at how a plug-in gets called in the app itself – here’s the command loader from DjangoSVN that loads up a plug-in:

def CommandHook(command, args, dsvnobj):
    """
    This will take the command and args variables, check a module list
    in the COMMAND_PATH directory for a new hook
    if it finds one it will kick off the execute_command() function after
    importing the module
   
    Module loading is dynamic, so more commands can be added as needed!
    """

   
    commands_dir = os.listdir(COMMAND_PATH)
    found = False
   
    if command.lower() in commands_dir:    
        hook_module = "from command_list." + command.lower() + ".hook import hook"
        try:
            logme('Importing hook classn')
            exec hook_module
            logme('Import completen')
            found = True
        except Exception, e:
            found = False
            logme('Import failed:' + str(e))
               
    if found:
        logme("FOUND HOOK: " + str(command))
        try:
            logme('Executing hook...n')
            thishook = hook(args=args, dsvnobj=dsvnobj)
            thishook.execute_hook()
        except Exception, e:
            logme('Execute failed... n')
            logme(str(e))

What is happening here is this:

  1. Get a list of the plug-ins that you have created in a directory – the plug-in name is the name of the directory (they are basically python modules)
  2. If the plug-in we’re looking for is in the commands[] dictionary, set up a load statement as a string. It’s important to get the parent module here – it might be worth it dynamically generating this if you don;t have your app in you PYTHONPATH
  3. Run exec hook_module – this will actually run the string as if it were typed in the interpreter
  4. Try / Catch that thing to ensure that you can shoot out an error if it doesn’t work
  5. Run the hooks main function – in the case of our interface, we use execute_hook()

That’s it – essentially this is all that is required to make your app plugable.

To make it more Django-specific, we include the following lines in the actual hook to load up the ORM machinery:

import sys, os
from dsvn.svnwrapper.dsvn_hook import dsvn_hook

class hook(dsvn_hook):
   
    def execute_hook(self):
        #Put what you like in here...
       
        #Initialise Django settings
        from django.core.management import setup_environ   
        from dsvn import settings
        setup_environ(settings)
       
        self.do_something()

Here we load up the django machinery in the init function and can essentially bolt-on a django app as a manager to your plug-in using your normal settings.py file!

How would you improve this model? Do you have ideas on making a plug-in framework that can be re-used in python easily?