Paste Script: Development

author:

Ian Bicking <ianb@colorstudy.com>

revision:

$Rev$

date:

$LastChangedDate$

Introduction

This document is an introduction to how you can extend paster and Paste Script for your system – be it a framework, server setup, or whatever else you want to do.

What Paste Script Can Do

paster is a two-level command, where the second level (e.g., paster help, paster create, etc) is pluggable.

Commands are attached to Python Eggs, i.e., to the package you distribute and someone installs. The commands are identified using entry points.

To make your command available do something like this in your setup.py file:

from setuptools import setup
setup(...
    entry_points="""
    [paste.paster_command]
    mycommand = mypackage.mycommand:MyCommand

    [paste.global_paster_command]
    myglobal = mypackage.myglobal:MyGlobalCommand
    """)

This means that paster mycommand will run the MyCommand command located in the mypackage.mycommand module. Similarly with paster myglobal. The distinction between these two entry points is that the first will only be usable when paster is run inside a project that is identified as using your project, while the second will be globally available as a command as soon as your package is installed.

How’s the Local Thing Work?

So if you have a local command, how does it get enabled? If the person is running paster inside their project directory, paster will look in Project_Name.egg-info/paster_plugins.txt which is a list of project names (the name of your package) whose commands should be made available.

This is for frameworks, so frameworks can add commands to paster that only apply to projects that use that framework.

What Do Commands Look Like?

The command objects (like MyCommand) are subclasses of paste.script.command.Command. You can look at that class to get an idea, but a basic outline looks like this:

from paste.script import command

class MyCommand(command.Command):

    max_args = 1
    min_args = 1

    usage = "NAME"
    summary = "Say hello!"
    group_name = "My Package Name"

    parser = command.Command.standard_parser(verbose=True)
    parser.add_option('--goodbye',
                      action='store_true',
                      dest='goodbye',
                      help="Say 'Goodbye' instead")

    def command(self):
        name = self.args[0]
        if self.verbose:
            print "Got name: %r" % name
        if self.options.goodbye:
            print "Goodbye", name
        else:
            print "Hello", name

max_args and min_args are used to give error messages. You can also raise command.BadCommand(msg) if the arguments are incorrect in some way. (Use None here to give no restriction)

The usage variable means paster mycommand -h will give a usage of paster mycommand [options] NAME. summary is used with paster help (describing your command in a short form). group_name is used to group commands together for paste help under that title.

The parser object is an optparse <http://python.org/doc/current/lib/module-optparse.html> OptionParser object. Command.standard_parser is a class method that creates normal options, and enables options based on these keyword (boolean) arguments: verbose, interactive, no_interactive (if interactive is the default), simulate, quiet (undoes verbose), and overwrite. You can create the parser however you want, but using standard_parser() encourages a consistent set of shared options across commands.

When your command is run, .command() is called. As you can see, the options are in self.options and the positional arguments are in self.args. Some options are turned into instance variables – especially self.verbose and self.simulate (even if you haven’t chosen to use those options, many methods expect to find some value there, which is why they are turned into instance variables).

There are quite a few useful methods you can use in your command. See the Command class for a complete list. Some particulars:

run_command(cmd, arg1, arg2, ..., cwd=os.getcwd(), capture_stderr=False):

Runs the command, respecting verbosity and simulation. Will raise an error if the command doesn’t exit with a 0 code.

insert_into_file(filename, marker_name, text, indent=False):

Inserts a line of text into the file, looking for a marker like -*- marker_name -*- (and inserting just after it). If indent=True, then the line will be indented at the same level as the marker line.

ensure_dir(dir, svn_add=True):

Ensures that the directory exists. If svn_add is true and the parent directory has an .svn directory, add the new directory to Subversion.

ensure_file(filename, content, svn_add=True):

Ensure the file exists with the given content. Will ask the user before overwriting a file if --interactive has been given.

Templates

The other pluggable part is “templates”. These are used to create new projects. Paste Script includes one template itself: basic_package which creates a new setuptools package.

To enable, add to setup.py:

setup(...
    entry_points="""
    [paste.paster_create_template]
    framework = framework.templates:FrameworkTemplate
    """)

FrameworkTemplate should be a subclass of paste.script.templates.Template. An easy way to do this is simply with:

from paste.script import templates

class FrameworkTemplate(templates.Template):

    egg_plugins = ['Framework']
    summary = 'Template for creating a basic Framework package'
    required_templates = ['basic_package']
    _template_dir = 'template'
    use_cheetah = True

egg_plugins will add Framework to paste_plugins.txt in the package. required_template means those template will be run before this one (so in this case you’ll have a complete package ready, and you can just write your framework files to it). _template_dir is a module-relative directory to find your source files.

The source files are just a directory of files that will be copied into place, potentially with variable substitutions. Three variables are expected: project is the project name (e.g., Project-Name), package is the Python package in that project (e.g., projectname) and egg is the project’s egg name as generated by setuptools (e.g., Project_Name). Users can add other variables by adding foo=bar arguments to paster create.

Filenames are substituted with +var_name+, e.g., +package+ is the package directory.

If a file in the template directory ends in _tmpl then it will be substituted. If use_cheetah is true, then it’s treated as a Cheetah template. Otherwise string.Template is used, though full expressions are allowed in ${expr} instead of just variables.

See the templates module for more.