The Web Framework Shootout

Contents

Introduction

In the beginning for the Python web programmer there were two choices: Zope and the cgi module. On one hand you had a featureful but complex application environment, on the other a simple but featureless and low-level module. For a significant number of web applications Zope's features weren't helpful and the complexity daunting, but the alternative was discouragingly primitive.

In response to this a variety of web application frameworks have been developed in the last few years, often by developers who created a framework in the process of their own application development. I try in this paper to show the flavor of these alternatives, and to inform the developer that's trying to decide on a framework for their application.

About the Author and a Disclaimer

Ian Bicking is an independent web developer and an active contributor to Webware, one of the frameworks covered in this paper.

Motivation

This paper's title comes from a suggestion on the Python WebProgramming Wiki, but this paper does not intend to present a fight-to-the-death among the frameworks. This paper is more a whirlwind tour.

This paper also does not include a blow-by-blow of each framework's features, formed in tables filled with Y/N. I don't believe developers who have chosen Python will choose a technology based solely on features -- Python compares well to other languages, but if each of us based our choice of language on features we would all be programming Java.

Frameworks, by definition, are the context in which you will be programming. They force a certain structure (some frameworks are more forceful than others), and they make some things easier and some things harder. You will be well served to find a framework that fits your style of programming and you expectations.

The examples are included to give you a concrete introduction to the framework. I hope they will serve as food for your intuition, hopefully even inspiring an emotional reaction. Here's to happy programmers!

About the Examples

To make the examples easier to compare side-by-side (and easier to understand), they all implement the same application: a simple Wiki.

Note

For those who are not familiar with the concept of a Wiki, in its most concrete form a Wiki is a set of pages where each page has a link to "edit this page", and any user can edit the contents of that page (though a web form). The pages are usually defined with some sort of text markup, and that markup includes easy linking between the Wiki pages. The original Wiki contains much more information.

Most of the logic for this Wiki exists in an framework-neutral module. For the sake of brevity these examples we will only show the display and editing of pages. A more complete application (like MoinMoin or ZWiki) might include searching, change tracking, user management, etc.

These examples aren't meant to be measures of the frameworks. A framework that results in short and simple for a simple application may not scale well to a more realistic example. It's left to the reader to extrapolate.

Wiki.py Interface

The Wiki module defines the logic that all the pages share. The WikiPage class is most interesting; it's methods and properties are:

__init__(pageName):
Each page has a name, which is a unique identifier. For example, "frontpage".
exists():
Does this page have content yet?
html:
The HTML for the page.
preview(text):
Returns the HTML for the given text, but doesn't save the text.
text:
The text of the page. ReStructuredText is used, though the parsing is internal to the module. You can assign to this property to save new text for the page.
baseHref:
WikiPage has to construct internal links, which it does by appending the page name to this class variable.

These provide all the methods necessary to view, edit, and save wiki pages. The actual module remains fairly short, and most of the hard work of parsing is done by ReStructuredText (a component of docutils).

Background for the Frameworks

While this paper will cover each framework individually, some aspects are best compared directly.

Application Servers

Many of the frameworks have something that might qualify as an application server. An application server is a persistent control program which listens for requests (directly or indirectly), and when a request is received it bundles up the request and dispatches it (to your application code).

To handle concurrent requests, the application server can either use multiple threads (or multiple processes, in the case of SkunkWeb), or an asynchronous server.

In the case of threads or processes, there is a master thread/process that listens for new requests, and when a request is received it spawns a new thread or process to handle the request. Each handler can take as long as it needs to respond. Any shared resources generally require careful attention to issues of concurrency, because more than one instance of the code is running at the same time and may be accessing the same data. Otherwise this system is fairly easy to work with. Webware and Zope are both multi-threaded, while SkunkWeb uses multiple processes.

Other application servers use asynchronous techniques, where the application server deals with one request before starting the next. Requests are not done entirely serially, each request finishing completely before the next one can run, but your code will generally run in serial, with the application server doing its work in the spaces inbetween.

The justification for this technique is that by removing the overhead of multiple threads or processes the application can respond that much faster to requests. It also avoids some of the difficulty of programming multi-threaded code -- since requests aren't handled concurrently you avoid many potentially difficult bugs when using shared resources. This model is used by CherryPy, Twisted, and BaseHTTPServer. All of these have problems with long-running algorithms, such as a complicated computation or a database query that may take a long time to run. Typically you will have to introduce threads and callbacks to resolve this problem.

Quixote offers an application server in several environments, as well as use as a standalone program.

Standalone scripts

A framework does not have to use an application server. CGI is the most prominent example, and the other frameworks are generally rooted in a CGI-like execution model.

The basic CGI model is one where each request runs a new program. Instead of having an application server dispatch requests, requests go directly to your web application code, and it's up to you to then give control to the framework. I call these standalone scripts because in effect each page is a program of its own.

The code you write for these frameworks doesn't have to look that much different than it would with an application server. In some cases the only real difference may be a couple lines at the bottom of each module that invoke the parent framework.

The CGI model in particular can be very inefficient. The Python interpreter and modules take a considerable amount of time to load, which only gets worse as features are added to the framework or to your own application. There are far fewer options for optimizing CGI scripts compared with long-running processes (as you'll have with an application server). The problem is significant.

Enter mod_python, which provides an upgrade path from CGI. Mod_python embeds a Python interpreter in the Apache process. Generally a mod_python application will still start anew with each request, but with the interpreter and all the modules will already be loaded, which is the larger part of the overhead. There are also several optimization techniques available (most optimizations in any framework will boil down to caching).

The frameworks jonpy, Albatross, Spyce, and Slither (not covered here) all offer both CGI and mod_python options. CGI programs can usually be ported to mod_python fairly easily.

Hosting

Hosting Python web applications is something of a problem. On most Unix (Linux/FreeBSD/etc) hosts Python CGI-based frameworks will be fairly easy to host -- most modern versions of these operating systems include Python, and that is the only requirement for supporting applications written for CGI.

Frameworks based on an application server generally require specific support by the hosting provider. An application server is a long-running process, and are typically not allowed in shared hosting situations: an application server looks similar to a runaway process or a server (such as IRC) that is not allowed as part of the hosting agreement, and such processes are often killed by an automated process.

In summary, if you want to use commodity-level hosting you will probably find a CGI-based framework much easier to set up. The PythonHosting wiki page will help you find hosts for more advanced framework setups.

The Application Servers

BaseHTTPServer

BaseHTTPServer is a standard Python module. While it provides some abstraction for an HTTP server, it is quite low-level. It contains no assumption about URLs mapping to files, or even a concept of query strings or other typical HTTP abstractions. It is included as something of a novelty, but also as a control to this experiment, the simplest framework in this paper. It also helps to show that the scope of this Wiki application is very small -- given no facilities and very little in the way of a framework we can write a Wiki server without much more code than we'd use with a framework. Obviously this would not scale -- the simplicity of a web application of this size says little about the simplicity possible with a more significant application.


#!/usr/bin/python

# We use the cgi module to parse the submitted form data
import BaseHTTPServer, cgi
from Wiki import WikiPage

class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    # do_GET or do_POST are the entry points -- they are called
    # by BaseHTTPRequestHandler to start things off.  In them we
    # will first identify the query string, and chop any query
    # string off the path if necessary.
    
    def do_GET(self):
        if self.path.find('?') != -1:
            self.path, self.query_string = self.path.split('?', 1)
        else:
            self.query_string = ''
        self.respond()

    # Essentially a POST contains variables in just the same format as
    # the GET query string, just passed in the body of the request.
    # Potentially it could be in a different format (especially with
    # file uploads), but we're not making a fully compliant server
    # here.
    def do_POST(self):
        self.query_string = self.rfile.read(int(self.headers['Content-Length']))
        self.respond()

    def respond(self):
        if not self.path[1:]: # first character is a "/"
            self.page = WikiPage('frontpage')
        else:
            self.page = WikiPage(self.path[1:])
        # We use the cgi module to parse the query string:
        self.args = dict(cgi.parse_qsl(self.query_string))

        self.send_response('200', 'OK')
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.action = self.args.get('action')
        if self.action == 'edit':
            self.edit()
        elif self.action in ('Save', 'Preview'):
            self.save()
        else:
            self.display()

    # The page's HTML header and footer:
    def writeHeader(self, title):
        self.wfile.write('<html><head><title>%s</title></head>\n'
                         % title)
        self.wfile.write('<body><h1>%s</h1>\n' % title)

    def writeFooter(self):
        self.wfile.write('</body></html>')

    # In these methods we are doing all the same logic we'll do in all
    # these examples, displaying the page, handling the save and
    # preview, etc.  All in pure Python in this example.  Text written
    # to self.wfile will go directly to the viewer's browser.

    def display(self):
        self.writeHeader(self.page.name)
        self.wfile.write(self.page.html)
        self.wfile.write('''<hr noshade>
        <a href="/%(name)s?action=edit">Edit %(name)s</a><br>
        <a href="/frontpage">Front Page</a>
        ''' % {'name': self.page.name})
        self.self.writeFooter()

    def edit(self, text=None):
        self.writeHeader('Edit %s' % self.page.name)
        if text:
            self.wfile.write(self.page.preview(text))
            self.wfile.write('<hr noshade>\n')
        self.wfile.write('''<form action="%s" method="POST">
        <textarea name="text" rows=10 cols=50 style="width: 90%%">%s</textarea>
        <br>
        <input type="submit" name="action" value="Save">
        <input type="submit" name="action" value="Preview">
        <input type="submit" name="action" value="Cancel">
        </form>''' % (self.page.name, cgi.escape(text or self.page.text)))
        self.self.writeFooter()

    def save(self):
        if self.action == 'Preview':
            self.edit(self.args['text'])
            return
        self.page.text = self.args['text']
        self.display()

# When run as a script we start the server on port 8080:
if __name__ == '__main__':
    server_address = ('', 8080)
    httpd = BaseHTTPServer.HTTPServer(server_address, HTTPHandler)
    httpd.serve_forever()

CherryPy

http://cherrypy.org

CherryPy pages are written in a form very much like Python classes, but with sections for methods with different roles. It's easiest to explain with the example:


from Wiki import WikiPage

# This function is called at the beginning of every request.
# We use it to manipulate the URL, turning something like
# /frontpage/edit into /edit?name=frontpage (which will call
# the "edit" view/mask, with a "name" argument).
def initNonStaticRequest():
    # request is a global variable
    if request.path:
        parts = request.path.split('/', 2)
        # request.paramMap is a dictionary of GET/POST variables
        request.paramMap['name'] = parts[0]
        if len(parts) == 1:
            request.path = ''
        else:
            request.path = parts[1]

# CherryPy uses a special class statement, which is split into
# different sections (aspect, function, mask, view).
CherryClass Site:

# "aspects" are handled at compile time -- the code in start: and end:
# are prepended and appended to the body of methods of the class.
# Each "aspect" starts with an expression that is evaluated with each
# function name, to determine if that code should be appended to that
# function.  Essentially this is a macro preprocessor, but can be used
# for a standard look.
aspect:

    # We are just adding this code to the masks "index" and "edit",
    # these are the only methods that actually generate pages.
    # If we didn't restrict this then even functions like setup()
    # or title() would have this code prepended/appended.
    (function.name in ('index', 'edit')) start:
        self.setup(name)
	# The use of "_page" is something of a hack -- it so happens
        # that the compiled version of this site uses _page to
        # accumulate the output.
        _page.append("<html><head><title>%s</title></head><body>\n"
                     % self.page.name)
        _page.append("<h1>%s</h1>\n" % self.page.name)

    (function.name in ('index', 'edit')) end:
        _page.append("</body></html>")

# The function section is essentially the normal section, where
# we just define normal methods.
function:

    def setup(self, name):
        self.page = WikiPage(name)
        WikiPage.baseHref = '/'

CherryClass Root(Site):

# Masks are essentially templates.  The body of the masks are HTML,
# with instructions added.  Like Zope's Page Templates, it uses
# attributes of the HTML tags for the template instructions:
# py-eval replaces the contents of the tag with the result of the
# expression, py-if removes the contents if the expression evaluates
# false, py-for is used for looping, and py-attr replaces the value
# of the attribute that comes right after it.  
mask:

    # index is the default mask/view, i.e., the site root.
    def index(self, name="frontpage"):
        <!-- if the output includes empty <span> tags, the span tag
             is removed, as will happen in this case: -->
        <span py-eval="self.page.html"></span>
        <hr noshade>
        <a py-attr="'/%s/edit' % name" href="">Edit <py-eval="name"></a><br>
        <a href="/frontpage">Front Page</a>

    def edit(self, name, text=None):
        <div py-if="text">
          <span py-eval="self.page.preview(text)">Preview text</span>
          <hr noshade>
        </div>
        <form py-attr="'/%s/save' % self.page.name" action="" method="POST">
        <textarea name="text" rows=10 cols=50 style="width: 90%"
         py-eval="text or self.page.text"></textarea>
        <br>
        <input type="submit" name="action" value="save">
        <input type="submit" name="action" value="preview">
        <input type="submit" name="action" value="cancel">
        </form>

# Views are essentially functions, and there isn't much magic done to
# them (except aspects).  They really only differ from functions in
# that they can be accessed as a URL, i.e., they are public.
view:

    def save(self, name, text, action='save'):
        self.setup(name)
        if action == 'preview':
            return self.edit(name, text)
        elif action != 'cancel':
            self.page.text = text
        # response (and request) are global variables we have
        # access to whenever we desire...
        response.headerMap['status'] = 302
        response.headerMap['location'] = '/%s' % self.page.name
        return ''

CherryPy classes are compiled into standalone HTTP servers (this makes evaluating CherryPy very easy!) This compilation step allows some novel ideas, like aspects, but also breaks some of the basic principles of Python. Python classes are not really declared, but executed and constructed; because of the compilation phase, CherryClasses are somewhat fragile, at least if you try to use them in a more advanced or complex manner. As with any compiled situation, the error messages won't match up exactly with your code, however it is fairly easy to match the generated code to your original CherryPy classes.

Twisted

http://twistedmatrix.com

Like jonpy and Albatross, I won't expand fully on Twisted.

Twisted isn't specifically a web server, but is a framework for servers in general -- so it can be a server for IRC chats, an FTP server, a mail server, etc. HTTP is a relatively simple protocol, so of course Twisted supports it as well (HTTP is also the basis for applications like Gnutella that really have no other relation to the web).

Which is to say, Twisted is not the web framework, it is one level removed, the framework in which you can build another framework. Twisted does include a framework called Woven, which uses a strict MVC design. Woven is currently a moving target and not well documented -- it is more of an experiment than an alternative for actual development. Most of the behavior of Woven applications is implicit; a model, view, and controller are composed, and they interact through internal interfaces. This makes Woven very hard to understand.

Twisted has support for other frameworks, such as Quixote and Web Widgets (another Twisted-native framework). In my estimation, Twisted would be most successful as a web server by porting other frameworks.

Skunkweb

http://skunkweb.sf.net

SkunkWeb is in many ways the least Pythonic of these frameworks, rivaled perhaps by Zope. STML, SkunkWeb's templating language, is very central to the framework, and though it's similar to Python it also differs from Python in subtle ways.

STML at first looks like HTML markup -- STML tags look like <:tagname attr=value:> -- but it's more like a Python function call. The attributes are like named parameters -- and just like Python's named parameters you can use them as positional parameters. The values for the attributes are generally assumed to be strings unless they are surrounded by backquotes, in which case they are evaluated as Python expressions. Tags exist for all the normal template functionality -- value insertion, conditionals, loops, etc.

As noted in Execution Model, SkunkWeb is unique among application servers in running each transaction in a separate process. In general SkunkWeb has the most features for ensuring the robustness of the application server, even if there are bugs in the application code. The application server maintains its integrity even if the subprocess has a segmentation fault (which will generally only happen when using modules written in C), and a time limit can be put on subprocesses.

The rest I'll explain in the example (<:* ... *:> indicates a comment):

<:* :args gives the "arguments" this page accepts, in other words the
    variables the page expects (passed as query parameters or
    POST variables).  In addition to defining the variables, we
    could also convert their types or indicate defaults (we've
    indicated a default for "name" -- the string "frontpage").
    None is otherwise the implicit default. *:>
<:args action
       text
       name=frontpage::>

<:* Equivalent to "from Wiki import WikiPage": *:>
<:import Wiki WikiPage::>
<:call `WikiPage.baseHref='/skunk/wiki.html?name='`::>

<:* Equivalent to "page = WikiPage(name)": *:>
<:set page `WikiPage(name)`::>

<:* Because you cannot define functions inside of a template,
    we don't factor this page into any sort of functions.  You
    can create function-like templates called Components, but
    each component must be in its own file, which would be more
    trouble than it's worth for this simple example. *:>

<html><head>
<:* :val inserts the value of the expression: *:>
<title><:val `action`::> <:val `name`::></title>
</head><body>
<h1><:val `action`::> <:val `name`::></h1>
<:if `action=='Edit' or action=='Preview'`::>
  <:if `action=='Preview'`::>
    <:val `page.preview(text)`::>
    <hr noshade>
  <:/if:>
  <form action="wiki.html" method=POST>
    <:* This is a shortcut for making hidden fields, this is
        equivalent to:
        <input type="hidden" name="name" value="<:val :> `name`:>"> *:>
    <:hidden name=`name`::>
    <:* "fmt=html" quotes text, replacing < with &lt;, etc. *:>
    <textarea name="text" rows=10 cols=50
     style="width: 90%"><:val `text or page.text` fmt=html::></textarea>
    <br>
    <input type="submit" name="action" value="Save">
    <input type="submit" name="action" value="Preview">
  </form>
  <:set links `[]`::>
<:else ::>
  <:if `action=='Save'`::>
    <:call `page.text=text`::>
  <:/if:>
  <:val `page.html`::>
  <:* Here we express the link as a dictionary of GET variables that
      will be added to the URL -- STML will convert the dictionary
      into ?action=Edit&name=`name` for us. *:>
  <:set links `[('Edit %s' % name, {'action': 'Edit', 'name': name}),
                ('Front Page', {'name': 'frontpage'})]`::>
<:/if:>

<:if `links`::>
  <hr noshade>
  <:* Equivalent to "for link in links": *:>
  <:for `links` link::>
    <:url /skunk/wiki.html queryargs=`link[1]` text=`link[0]` noescape=yes::><br>
  <:/for:>
<:/if:>

</body></html>

Webware

http://webware.sf.net

Webware is a relatively old framework at the ripe age of three. Webware was inspired by Java servlets and Web Objects, and like many of these was created somewhat in reaction to Zope.

The primary metaphor for Webware is the servlet. This is a class that represents a page and handles a request. For each page in the application there is a separate class.

Servlets can inherit from each other, which can serve as the basis for sharing code among separate pages. While we don't show PSP (a JSP-like plug-in for embedding Python in HTML code), a common technique is to have a PSP page inherit from a Python class that defines application code.

Webware emphasizes modularity. Many of the included components are independent of the framework, though included for convenience. The Cheetah template language, for example, was originally developed with Webware in mind, but was developed without any dependencies; you can now use Cheetah templates in several of the other frameworks as a result.

In this example we use just a single servlet:


from Wiki import WikiPage
from WebKit.Page import Page

# We subclass Page -- Page is a servlet skeleton that also generates
# the skeleton of an HTML page for us, so we only need to define how
# we want to body of the page to look.
class index(Page):

    def awake(self, transaction):
       # The awake method is called at the beginning of every
       # transaction.  It is a typical place to process any
       # form input.
       Page.awake(self, transaction)
       # extraURLPath gives the portion of the URL that comes
       # after the servlet name.
       name = self.request().extraURLPath()[1:]
       if not name:
           self.response().sendRedirect('FrontPage')
       else:
           self.page = WikiPage(name)

   def actions(self):
       # Page searches for any parameters named _action_XXX, where XXX
       # is the name of a method of the servlet.  That method is then
       # called in lieu of writeContent (the normal method that gets
       # called).  Here we give a list of legal action methods.
       return ['edit', 'preview', 'save']

   def title(self):
       # The Page class generates the <title> (and head) for us, but
       # we have to give it the actual contents of the title
       return self.page.title

   def writeContent(self):
       # self.write() writes the output.  Simply writing strings work
       # well for something this small -- you might use a template or
       # PSP for something more significant.
       self.write(self.page.html)
       self.write('<hr noshade>\n')
       # Note that _action_edit here will cause this link to call the
       # edit method:
       self.write('<a href="%s?_action_edit=yes">Edit this page</a><br>\n'
                  % self.page.name)
       self.write('<a href="frontpage">Front Page</a>')

   def edit(self, text=None):
       if text is None:
           text = self.page.text
       self.write('''<form action="%s" method="POST">
       <textarea name="text" rows=10 cols=50 style="width: 90%">%s</textarea>
       <br>
       <input type="submit" name="_action_save" value="Save">
       <input type="submit" name="_action_preview" value="Preview">
       </form>'''
                  % (self.page.name, self.htmlEncode(text)))

   def preview(self):
       text = self.request().field('text')
       self.write(page.preview(text))
       self.write('<hr noshade>\n')
       self.edit(text)

   def save(self):
       self.page.text = self.request().field('text')
       self.writeContent()

Zope

http://zope.org

I won't write much about Zope -- we all know Zope, and we probably each probably have our opinions about it. Zope is notable in that it's the only framework here that makes it difficult to use external modules. The other frameworks all depend on OS-level security to protect the system, while Zope is unique in protecting the system from itself. The result is an application that is rather insular, and requires that software be written specifically for Zope in order to work in Zope.

In order to use WikiPage inside Zope we have to add some code. First, because Zope does not currently run under Python 2.2, we replace the properties with methods (e.g., .html()), making it 2.1-compatible. Then we subclass it to add security information:


from AccessControl import ClassSecurityInfo
from Globals import InitializeClass

class ZWikiPage(WikiPage):

    security = ClassSecurityInfo()
    security.setDefaultAccess("allow")
    security.declarePublic('html', 'text', 'setText',
                           'setBaseHref', 'css')

InitializeClass(WikiPage)

# External methods have to be functions, so we need a function that
# will return the ZWikiPage instance, instead of calling the
# ZWikiPage class directly.
def getWikiPage(name):
    return WikiPage(name)

By putting this into the Zope Extensions/ directory, we can import the getWikiPage function as an External Method. Though it's not the only option, I implement the rest of the application in Page Templates (it could also be done in DTML, or the entire application could be a Product):

index_html: (a Page Template)

<!-- index_html: Page Template -->
<html metal:use-macro="here/standard_template.pt/macros/page">
  <div metal:fill-slot="body" 
       tal:define="name request/name | string:frontpage; 
                   page python:container.WikiPage(name)">
    <span tal:replace="structure page/html">HTML</span>
    <hr noshade>
    <a tal:attributes="href python:'edit?name=%s' % name" 
       href="">Edit this page</a><br>
    <a href="./?name=frontpage">Front Page</a>
  </div>
</html>

edit: (a Page Template)

<!-- edit: Page Template -->
<html metal:use-macro="here/standard_template.pt/macros/page">
  <div metal:fill-slot="body"
       tal:define="name request/name;
                   page python:container.WikiPage(name)">

    <span tal:condition="options/preview | nothing">
      <span tal:content="structure options/preview">Preview</span>
      <hr noshade>
    </span>
          
    <form action="save" method="POST">
      <input type="hidden" name="name"
             tal:attributes="value request/name">
      <textarea name="text" rows=10 cols=50 style="width: 90%"
             tal:content="python:page.text()">
      Page Text</textarea>
      <br>
      <input type="submit" value="Save">
      <input type="submit" name="preview" value="Preview">
      <input type="submit" name="cancel" value="Cancel">
    </form>
  </div>
</html>

save: (a Python Script)


# save: a Python Script
request = container.REQUEST

text = request['text']
page = container.WikiPage(request['name'])

if request.get('preview'):
    return container.edit(preview=page.preview(text))

if not request.get('cancel'):
    page.setText(text)

return container.index_html()

Quixote

http://www.mems-exchange.org/software/quixote

Quixote is a programmer's framework. It was originally developed for programming a complex site with little need for aesthetics, and this priority can be seen in the result. While Quixote is not particularly resistant to web designers and pretty sites, the documentation doesn't spend much time on that, and the conveniences that Quixote provides are really oriented towards Python programmers.

Quixote templates are just normal classes or functions, with a small twist, best explained in an example:


def orderedList [plain] (lst):
    "<ol>\n"
    for item in lst:
        "<li>%s\n" % item
    "</ol>\n"

orderedList(['apple', 'banana'])
# Returns: "<ol>\n<li>apple\n<li>banana\n</ol>\n"

PTL functions or methods are indicated with [plain] or [html] after the function name, so they can coexist with normal functions and methods in the same file. When using [html] Quixote will quote everything except literal strings and explicit HTML content; potentially making the HTML generation a little more robust.

Because Quixote is explicit about what it allows, it is fairly easy to publish the WikiPage class directly. The rest is explained in the example:


_q_exports = []
from Wiki import WikiPage
import cgi

# _q_getname is a hook that gives us an opportunity to redirect
# the URLs.  In this case, instead of letting the URL name an
# object, we use the URL (which will contain the page name) as
# an argument to QWikiPage.
def _q_getname(request, name):
    return QWikiPage(request, name)


class QWikiPage(WikiPage):

    # Only these methods (and implicitly _q_index) will be
    # published.  Because we explicitly indicate which methods
    # are published, it's easy to publish our WikiPage class
    # directly without worrying about method overlap.
    _q_exports = ['edit', 'preview', 'save']

    baseHref = '/wiki/Quixote/wiki.cgi/page/'

    def __init__(self, request, name):
        WikiPage.__init__(self, name)
        self.request = request

    # We call the header and footer methods explicitly to
    # create a common look for the pages.  By having a single
    # base class for all our published classes, we can define
    # a common site look (but here we have only one
    # published class)
    def header [html] (self, prefix=''):
        '<html><head><title>'
        prefix
        ' '
        self.name
        '</title></head><body><h1>'
        self.name
        '</h1>'

    def footer [html] (self, links=[]):
        if links:
            '<hr noshade>\n'
            '<br>\n'.join(['<a href="%s">%s</a>' % (href, desc)
                      for href, desc in links])
        '</body></html>'

    # _q_index is the main method for displaying this class
    def _q_index(self, request):
        # Remember this is a subclass of WikiPage, so self.exists()
        # is actually being handled by WikiPage:
        return self.header() + self.html + \
               self.footer([('edit', 'Edit %s' % self.name),
                            ('../frontpage', 'FrontPage'),
                            ])

    def edit(self, request, text=None):
        if text is None:
            text = self.text
        return '''%s<form action="%s/save" method="POST">
        <textarea name="text" rows=10 cols=50 style="width: 90%%">%s</textarea>
        <br>
        <input type="submit" value="Save">
        <input type="submit" name="preview" value="Preview">
        <input type="submit" name="cancel" value="Cancel">
        </form>%s''' % (self.header('Edit'),
                        request.get_url(1),
                        cgi.escape(text),
                        self.footer())

    def preview(self, request):
        # request.form contains all the GET/POST variables:
        text = request.form['text']
        return "%s%s\n<hr noshade>\n%s%s" \
               % (self.header('Preview'),
                  WikiPage.preview(self, text),
                  self.edit(request, text),
                  self.footer())

    def save(self, request):
        # We test here which button they hit...
        if request.form.get('preview'):
            return self.preview(request)
        if request.form.get('cancel'):
            # request.get_url(N) returns the URL with N parts
            # stripped off, so for /frontpage/save it returns
            # just /frontpage
            request.redirect(request.get_url(1))
            # We always have to return some string:
            return ''
        self.text = request.form['text']
        request.redirect(request.get_url(1))
        return ''

Quixote can use a CGI interface, mod_python, or as a persistent server using FastCGI or its own protocol SCGI. As such it can be used in the style of an application server, or as a standalone script.

CGI and mod_python based Frameworks

cgi

The cgi module is the unframework. It gives you the tools you need, but that's it. CGI a common way to interface with web servers. Each request is turned into an invocation of your program, with the request data passed through environmental variables and the standard input, and script responds simply by printing back to the web server. This simplicity is one of the best features of CGI -- you can understand it completely. CGI scripts are also easy to install, and the cgi module comes with the standard Python installation.

One flaw of using plain CGI is performance. With each request your CGI script is loaded, and worse the entire Python interpreter is loaded. This works fine if you have a couple requests a second, but can quickly bog down after that. The overhead of loading the interpreter dwarfs any overhead of actually running your script.

CGI doesn't give you any special features. However, this can sometimes be quite efficient if you don't need these features. In this Wiki example, we actually need very few features:


#!/usr/bin/env python

import cgi, os
import cgitb; cgitb.enable()
import Wiki


# PATH_INFO contains the portion of the URL that comes after
# cgiwiki.cgi.  For all actions, this is the page we are using.
page = Wiki.WikiPage(os.environ['PATH_INFO'][1:])

# FieldStorage parses any fields that were submitted through forms.
# It's the primary interface for cgi.
form = cgi.FieldStorage()

# There's several different actions that may occur -- we
# may just display the page, we may edit it, save the edits
# that were submitted, or show a preview.  We define each of
# these as a function:

def printPage():
    if not page.exists():
        print "This page does not yet exist.  Create it:"
        printEdit()
    else:
        print page.html
        print '<hr noshade>'
        print '<a href="%s?edit=yes">Edit this page</a>' \
              % page.name

def printEdit(text=None):
    # The text argument is used in previews -- you want the
    # textarea to be loaded with the same text they submitted.
    if text is None:
        # This isn't a preview, so we get the page's text.
        text = page.text
    print '<h1>Edit %s</h1>' % page.title
    print '<form action="%s" method="POST">' % page.name
    print '<textarea name="text" rows=10 cols=50 style="width: 90%%">' \
          '%s</textarea><br>' % cgi.escape(text)
    print '<input type="submit" name="save" value="Save">'
    print '<input type="submit" name="preview" value="Preview">'
    print '</form>'

def printSave():
    page.text = form['text'].value
    printPage()

def printPreview():
    print '<h1>Preview %s</h1>' % page.title
    print page.previewText(form['text'].value)
    print '<hr noshade>'
    printEdit(form['text'].value)

# Here's where we display the actual page

# You must print headers first.  Content-type is the header
# you must always display -- it's usually text/html, since we
# are creating web pages.  The extra \n signifies we are
# finished with headers.
print "Content-type: text/html\n"
print "<html><head><title>%s</title>" % page.title
print Wiki.css
print "</head><body>"
# Here we use fields to determine what action we should take:
# edit, save, preview, or display
if form.has_key('edit'):
    printEdit()
elif form.has_key('save'):
    printSave()
elif form.has_key('preview'):
    printPreview()
else:
    printPage()
print "</body></html>"

Spyce

http://spyce.sf.net

Spyce is one among a family of systems that embeds Python in HTML, similar to PHP or ASP. Many of the others attempts have fallen by the wayside, never developed to their potential; the most active alternative at this time would be empy, though it is closer to a template language than an entire framework. Webware also includes a PSP plugin, which acts similarly (though with a slightly more JSP feel).

Many people react strongly against embedding code in HTML, often due to past PHP trauma. But as a form of templating, it's fairly easy to understand and use; that it can be used for more than just templating shouldn't be such a strong indictment. The real problem is when you are forced to implement logic in your templates because the template is the only entry point. Unfortunately Spyce suffers this problem, though with discipline the programmer can resist putting too much into the templates.

All of these frameworks (with the possible exception of Zope) allow easy use of normal Python modules, which can be used for as much of your logic as you wish. In the example Wiki module we put most of the logic into a class that makes no assumption about the framework (and besides the HTML output, doesn't even assume a web-based context). This Wiki module provides the basis for a Model-View-Controller factoring of your application, where the model (in this case the Wiki module) is complete abstracted from any interface.

Spyce doesn't introduce any problems using the model -- you simply import the module and use it as expected. But Spyce does not make the View-Controller distinction. In the context of a web application, the controller is generally the code to process form data, while the view is the HTML output. Spyce collapses everything into the view.

Spyce still has some important strengths. Besides the basic syntax which uses brackets (as opposed to ASP's <% %> or PHP's <? ?>), Spyce also has a taglib-like system, where new "active" tags can be added to the system. For instance, you could define a tag like <mytag:email> john@doe.com </mytag:email> that would expand to <a href="mailto:john@doe.com"> john@doe.com </a> (custom tags all have specific namespaces, so the email tag in this example is part of the mytag module that you would import). These tags can be nested and interspersed with normal Python substitutions (like <mytag:email>[[=email]]</mytag:email>).

The other interesting feature of Spyce is the ability to define functions. An example may be easiest to understand:

[[ # email template: emailLink = [[spy! address: [[=address]] ]] ]]

The resulting emailLink function can be used like any normal Python function. This allows much finer control of the site look than just a header and footer.

The example:

<!-- .import imports special Spyce modules, which can insert themselves
     into the request/response cycle: -->
[[.import name=redirect]]
[[.import name=transform]]

[[\
  from Wiki import WikiPage

  name = request.env('PATH_INFO')[1:] or 'frontpage'
  page = WikiPage(name)
]]

[[.include file=header.spy]]

<!-- The backslash marks a block of code that is properly indented,
     for extended Python code.  Otherwise whitespace is largely
     ignored. -->
[[\
if request['action'] == 'Save':
    page.text = request['text']
]]

<!-- Like PHP, you can go into and out of Python-mode, and like PHP
     it uses braces instead of indentation in this situation. -->
[[ if request['action'] == 'Preview': { ]]
  <h1 style="background-color: #990000; color: #ffffff">Preview</h1>
  [[= page.previewHTML(request['text']) ]]
  <hr noshade>
[[ } elif request['action'] == 'edit': { ]]
  <form action="[[=name]]" method="POST">
  <textarea name="text" rows=10 cols=50 wrap=soft 
   style="width: 80%">[[= transform.html_encode(page.text) ]]
  </textarea><br>
  <input type="submit" name="action" value="Save">
  <input type="submit" name="action" value="Preview">
  <input type="button" value="Cancel">
  </form>
[[ } else: { ]]
  [[=page.html]]
  <hr noshade>
  <a href="?action=edit">Edit [[=name]]</a><br>
  <a href="frontpage">Front Page</a>
[[ } ]]

[[.include file=footer.spy]]

header.spy:

<html>
  <head>
    <title>[[=name]]</title>
  </head>
  <body>
  <h1>[[=name]]</h1>

footer.spy:

  </body>
</html>

Albatross

http://www.object-craft.com.au/projects/albatross

We only mention Albatross[#]_, because its largely CGI/mod_python with a template system. The templating language looks very much like DTML (from Zope), with several additions to avoid some of DTML's problems. For instance, to display a list of links:

<al-for iter="link" expr="links">
  <al-a expr="link[0]"><al-value expr="link[1]"></al-a>
</al-for>

Albatross is very well documented and stable.

jonpy

http://jonpy.sf.net

Jon's Python modules are a set of fairly small modules to wrap CGI, mod_python, or FastCGI requests, and to do templating.

A basic handler looks like:


import jon.cgi

class Handler(jon.cgi.Handler):
    def process(self, req):
        req.set_header("Content-Type", "text/plain")
        req.write("Hello, %s!\n" % req.params.get("greet", "world"))

jon.cgi.CGIRequest(Handler).process()

The templating language is somewhat novel. It uses $$varname$$ for variable substitution, but looping and conditionals are done in a DOM-like style -- instead of the template including conditionals and loops, blocks of code are simply named like <!--wt:someblock--> ... <!--wt:/someblock-->. The template controller can output the block repeatedly with different substitution values, or not output the block at all.

Jonpy feels small and complete; its checked ambition should make it a stable basis for your application.

In Conclusion

Python is an excellent language for web development. It far exceeds Java in flexibility and ease, and far exceeds PHP in usability and robustness. It deserves to be a language of choice among a far larger group of developers than it currently is. Zope has enjoyed a great deal of success in content management, but there are still large areas of web development not well covered by Zope, and some of Python's best features of simplicity and predictability are lost when using the Zope environment.

It would be easier to see a path towards greater Python popularity in web programming if it was clear what underlying technology that would imply. PHP's web programming environment is built into the language. Java has defined a canonical environment through committee. Zope is the closest thing Python has to a canonical environment, but it does not seem to me that Zope is the philosophically representative of Python. Zope is an entity of its own.

In the meantime you, the enlightened programmer, can still choose for yourself to use Python. The community might not be as large as PHP's, but it only needs to be large enough. It may not be as fast as Java, but it only needs to be fast enough. Python is good enough where it has to be, and it can be truly great where it matters most. The same is true of these frameworks.