Chapter 9. Spring Python's plugin system

Spring Python's plugin system is designed to help you rapidly develop applications. Plugin-based solutions have been proven to enhance developer efficiency, with examples such as Grails and Eclipse being market leaders in usage and productivity.

This plugin solution was mainly inspired by the Grails demo presented by Graeme Rocher at the SpringOne Americas 2008 conference, in which he created a Twitter application in 40 minutes. Who wouldn't want to have something similar to support Spring Python development?

9.1. Introduction

Spring Python will manage an approved set of plugins. These are plugins written by the committers of Spring Python and are verified to work with an associated version of the library. These plugins are also hosted by the same services used to host Spring Python downloads, meaning they have the same level of support as Spring Python.

However, being an open source framework, developers have every right to code their own plugins. We fully support the concept of 3rd party plugins. We want to provide as much support in the way of documentation and extension points for you to develop your own plugins as well.

9.2. Coily - Spring Python's command-line tool

Coily is the command-line tool that utilizes the plugin system. It is similar to grails command-line tool, in that through a series of installed plugins, you are able to do many tasks, including build skeleton apps that you can later flesh out. If you look at the details of this app, you will find a sophisticated, command driven tool to built to manage plugins. The real power is in the plugins themselves.

9.2.1. Commands

To get started, all you need is a copy of coily installed in some directory located on your path.

% coily --help

The results should list available commands.

Coily - the command-line management tool for Spring Python
==========================================================
Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved
Licensed under the Apache License, Version 2.0


Usage: coily [command]

        --help                          print this help message
        --list-installed-plugins        list currently installed plugins
        --list-available-plugins        list plugins available for download
        --install-plugin [name]         install coily plugin
        --uninstall-plugin [name]       uninstall coily plugin
        --reinstall-plugin [name]       reinstall coily plugin
        
  • --help - Print out the help menu being displayed

  • --list-installed-plugins - list the plugins currently installed in this account. It is important to know that each plugin creates a directly underneath the user's home directory in a hidden directory .springpython. If you delete this entire directory, you have effectively uninstalled all plugins.

  • --list-available-plugins - list the plugins available for installation. Coily will check certain network locations, such as the S3 site used to host Spring Python downloads. It will also look on the local file system. This is in case you have a checked out copy of the plugins source code, and want to test things out without uploading to the network.

  • --install-plugin - install the named plugin. In this case, you don't have to specify a version number. Coily will figure out which version of the plugin you need, download it if necessary, and finally copy it into ~/.springpython.

  • --uninstall-plugin - uninstall the named plugin by deleting its entry from ~/.springpython

  • --reinstall-plugin - uninstall then install the plugin. This is particulary useful if you are working on a plugin, and need a shortcut step to deploy.

In this case, no plugins have been installed yet. Every installed plugin will list itself as another available command to run. If you have already installed the gen-cherrypy-app plugin, you will see it listed.

Coily - the command-line management tool for Spring Python
==========================================================
Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved
Licensed under the Apache License, Version 2.0


Usage: coily [command]

        --help                          print this help message
        --list-installed-plugins        list currently installed plugins
        --list-available-plugins        list plugins available for download
        --install-plugin [name]         install coily plugin
        --uninstall-plugin [name]       uninstall coily plugin
        --reinstall-plugin [name]       reinstall coily plugin
        --gen-cherrypy-app [name]       plugin to create skeleton CherryPy applications
        

You should notice an extra option listed at the bottom: gen-cherrypy-app is listed as another command with one argument. Later on, you can read official documentation on the existing plugins, and also how to write your own.

9.3. Officially Supported Plugins

This section documents plugins that are developed by the Spring Python team.

9.3.1. gen-cherrypy-app

This plugin is used to generate a skeleton CherryPy application based on feeding it a command-line argument.

9.3.1.1. External dependencies

gen-cherrypy-app plugin requires the installation of CherryPy 3.

% coily --gen-cherrypy-app twitterclone

This will generate a subdirectory twitterclone in the user's current directory. Inside twitterclone are several files, including twitterclone.py. If you run the app, you will see a working CherryPy application, with Spring Python security in place.

% cd twitterclone
% python twitterclone.py

You can immediately start modifying it to put in your features.

9.4. Writing your own plugin

9.4.1. Architecture of a plugin

A plugin is pretty simple in structure. It is basically a python package with some special things added on. gen-cherrypy-app plugin demonstrates this.

The special things needed to define a plugin are as follows:

  • A root folder with the same name as your plugin and a __init__.py, making the plugin a python package

  • A package-level variable named __description__

    This attribute should be assigned the string value description you want shown for your plugin when coily --help is run.

  • A package-level function named either create or apply

    • If your plugin needs one command line argument, define a create method with the following signature:

      def create(plugin_path, name)
      
    • If your plugin doesn't need any arguments, define an apply method with the following signature:

      def apply(plugin_path)
      

    In either case, your plugin gets passed an extra argument, plugin_path, which contains the directory the plugin is actually installed in. This is typically so you can reference other files your plugin needs access to.

[Note]What does "package-level" mean?

The code needs to be in the __init__.py file. This file makes the enclosing directory a python package.

9.4.2. Case Study - gen-cherrypy-app plugin

gen-cherrypy-app is a plugin used to build a CherryPy web application using Spring Python's feature set. It saves the developer from having to re-configure Spring Python's security module, coding CherryPy's engine, and so forth. This allows the developer to immediately start writing business code against a working application.

Using this plugin, we will de-construct this simple, template-based plugin. This will involve looking line-by-line at gen-cherrypy-app/__init__.py.

9.4.2.1. Source Code

"""
   Copyright 2006-2008 SpringSource (http://springsource.com), All Rights Reserved

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.       
"""
import re
import os
import shutil

__description__ = "plugin to create skeleton CherryPy applications"

def create(plugin_path, name):
    if not os.path.exists(name):
        print "Creating CherryPy skeleton app %s" % name
        os.makedirs(name)

        # Copy/transform the template files
        for file_name in ["cherrypy-app.py", "controller.py", "view.py", "app_context.py"]:
            input_file = open(plugin_path + "/" + file_name).read()

            # Iterate over a list of patterns, performing string substitution on the input file
            patterns_to_replace = [("name", name), ("properName", name[0].upper() + name[1:])]
            for pattern, replacement in patterns_to_replace:
                input_file = re.compile(r"\$\{%s}" % pattern).sub(replacement, input_file)

            output_filename = name + "/" + file_name
            if file_name == "cherrypy-app.py":
                output_filename = name + "/" + name + ".py"

            app = open(output_filename, "w")
            app.write(input_file)
            app.close()

        # Recursively copy other parts
        shutil.copytree(plugin_path + "/images", name + "/" + "images")
    else:
        print "There is already something called %s. ABORT!" % name

9.4.2.2. Deconstructing the factory

  • The opening section shows the copyright statement, which should tip you off that this is an official plugin.

  • __description__ is a required variable.

    __description__ = "plugin to create skeleton CherryPy applications"
    

    It contains the description displayed when a user runs:

    % coily --help
    
    Usage: coily [command]
    ...
            --gen-cherrypy-app [name]       plugin to create skeleton CherryPy applications
            
    
  • Opening line defines create with two arguments.

    def create(plugin_path, name):
    

    The arguments allow both the plugin path to be fed along with the command-line argument that is filled in when the user runs the command:

    % coily --gen-cherrypy-app [name]
    

    It is important to realize that plugin_path is needed in case the plugin needs to refer to any files inside its installed directory. This is because plugins are not installed anywhere on the PYTHONPATH, but instead, in the user's home directory underneath .springpython.

    This mechanism was chosen because it gives users an easy ability to pick which plugins they wish to use, without requiring system admin power. It also eliminates the need to deal with multiple versions of plugins being installed on your PYTHONPATH. This provides maximum flexibility which is needed in a development environment.

  • This plugin works by creating a directory in the user's current working directory, and putting all relevant files into it. The argument passed into the command-line is used as the name of an application, and the directory created has the same name.

        if not os.path.exists(name):
            print "Creating CherryPy skeleton app %s" % name
            os.makedirs(name)
    

    However, if the directory already exists, it won't proceed.

        else:
            print "There is already something called %s. ABORT!" % name
    
  • This plugin then iterates over a list of filenames, which happen to match the names of files found in the plugin's directory. These are essentially template files, intended to be copied into the target directory. However, the files are not copied directly. Instead they are opened and read into memory.

            # Copy/transform the template files
            for file_name in ["cherrypy-app.py", "controller.py", "view.py", "app_context.py"]:
                input_file = open(plugin_path + "/" + file_name).read()
    

    Then, the contents are scanned for key phrases, and substituted. In this case, the substitution is a variant of the name of the application being generated.

                # Iterate over a list of patterns, performing string substitution on the input file
                patterns_to_replace = [("name", name), ("properName", name[0].upper() + name[1:])]
                for pattern, replacement in patterns_to_replace:
                    input_file = re.compile(r"\$\{%s}" % pattern).sub(replacement, input_file)
    

    The substituted content is written to a new output file. In most cases, the original filename is also the target filename. However, the key file, cherrypy-app.py is renamed to the application's name.

                output_filename = name + "/" + file_name
                if file_name == "cherrypy-app.py":
                    output_filename = name + "/" + name + ".py"
    
                app = open(output_filename, "w")
                app.write(input_file)
                app.close()
    
  • Finally, the images directory is recursively copied into the target directory.

            # Recursively copy other parts
            shutil.copytree(plugin_path + "/images", name + "/" + "images")
    

9.4.2.3. Summary

All these steps effectively copy a set of files used to template an application. With this template approach, the major effort of developing this plugin is spent working on the templates themselves, not on this template factory. While this is mostly working with python code for a python solution, the fact that this is a template requires reinstalling the plugin everytime a change is made in order to test them.

Users are welcome to use gen-cherypy-app's __init__.py file to generate their own template solutions, and work on other skeleton tools or solutions.