Edgewall Software

Version 9 (modified by cmlenz, 17 years ago) (diff)

--

Genshi Tutorial

This tutorial is intended to give an introduction on how to use Genshi in your web application, and present common patterns and best practices. It is aimed at developers new to Genshi as well as those who've already used Genshi, but are looking for advice or inspiration on how to improve that usage.

Content

  1. Introduction
  2. Prerequisites
  3. Getting Started
  4. Basic Template Rendering
  5. Data Model
  6. Adding a Submission Form

Introduction

In this tutorial we'll create a simple Python web application based on CherryPy 3. CherryPy was chosen because it provides a convenient level of abstraction over raw CGI or WSGI development, but is less ambitious than full-stack web frameworks such as Pylons or Django, which tend to come with a preferred templating language, and often show significant bias towards that language.

The application is a stripped-down version of sites such as reddit or digg: it lets users submit links to online articles they find interesting, and then lets other users vote on those stories and post comments. Just for kicks, we'll call that application Geddit

The project is kept as simple as possible, while still showing many of Genshi features and how to best use them:

  • For persistence, we'll use native Python object serialization (via the pickle module), instead of an SQL database and an ORM.
  • There's no authentication of any kind. Anyone can submit links, anyone can comment.

Prerequisites

First, make sure you have CherryPy 3.0.x installed, as well as recent versions of FormEncode, Genshi (obviously), and Paste. You can download and install those manually, or just use easy_install:

$ easy_install CherryPy
$ easy_install FormEncode
$ easy_install Genshi
$ easy_install Paste

Getting Started

Next, set up the basic CherryPy application. Create a directory that should contain the application, and inside that directory create a Python package named geddit (basically a geddit directory containing an empty file called __init__.py. Inside that package, create a file called controller.py with the following content:

#!/usr/bin/env python

import os
import pickle
import sys

import cherrypy
from paste.evalexception.middleware import EvalException


class Root(object):

    def __init__(self, data):
        self.data = data

    @cherrypy.expose
    def index(self):
        return 'Geddit'


def main(filename):
    # load data from the pickle file, or initialize it to an empty list
    if os.path.exists(filename):
        fileobj = open(filename, 'rb')
        try:
            data = pickle.load(fileobj)
        finally:
            fileobj.close()
    else:
        data = []

    # save data back to the pickle file when the server is stopped
    def _save_data():
        fileobj = open(filename, 'wb')
        try:
            pickle.dump(data, fileobj)
        finally:
            fileobj.close()
    cherrypy.engine.on_stop_engine_list.append(_save_data)

    # Some global configuration; note that this could be moved into a configuration file
    cherrypy.config.update({
        'request.throw_errors': True,
        'tools.encode.on': True, 'tools.encode.encoding': 'utf-8',
        'tools.decode.on': True,
        'tools.trailing_slash.on': True,
        'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)),
    })

    # Initialize the application, and add EvalException for more helpful error messages
    app = cherrypy.Application(Root(data))
    app.wsgiapp.pipeline.append(('paste_exc', EvalException))
    cherrypy.quickstart(app, '/', {
        '/media': {
            'tools.staticdir.on': True,
            'tools.staticdir.dir': 'static'
        }
    })

if __name__ == '__main__':
    main(sys.argv[1])

Enter the tutorial directory in the terminal, and run:

$ PYTHONPATH=. python geddit/controller.py geddit.db

You should see a log message pointing you to the URL where the application is being served, which is usually http://localhost:8080/. Visiting that page will respond with just the string “Geddit”, as that's what the index() method of the Root object returns.

Note that we've configured CherryPy to serve static files from the geddit/static directory. CherryPy will complain that that directory does not exist, so create it, but leave it empty for now. We'll add static resources later on in the tutorial.

Basic Template Rendering

So far the code doesn't actually use Genshi, or even any kind of templating. Let's change that.

Inside of the geddit directory, create a directory called templates, and inside that directory create a file called index.html, with the following content:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <head>
    <title>$title</title>
  </head>
  <body>
    <h1>$title</h1>
    <p>Welcome!</p>
  </body>
</html>

This is basically an almost static HTML file with some simple variable substitution.

We now need to change the controller code so that this template is used. First, add the Genshi TemplateLoader to the imports at the top of the genshi/controller.py file, and instantiate a loader for the geddit/templates directory:

import cherrypy
from genshi.template import TemplateLoader
from paste.evalexception.middleware import EvalException

loader = TemplateLoader(
    os.path.join(os.path.dirname(__file__), 'templates'),
    auto_reload=True
)

Next, change the implementation of the index() method of the Root class to look like this:

    @cherrypy.expose
    def index(self):
        tmpl = loader.load('index.html')
        return tmpl.generate(title='Geddit').render('html', doctype='html')

This asks the template loader for a template named index.html, generates the output stream, and finally serializes the output to HTML. You you now reload the page in your browser, you should get back the following HTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <title>Geddit</title>
  </head>
  <body>
    <h1>Geddit</h1>
    <p>Welcome!</p>
  </body>
</html>

Data Model

To continue, we'll need to first add some classes to define the data model the application will use. As mentioned above, we're using a simple pickle file for persistence, so all we need to do here is create a couple of very simply Python classes.

Inside the geddit directory, create a file named model.py, with the following content:

from datetime import datetime


class Submission(object):

    def __init__(self, username, url, title):
        self.username = username
        self.url = url
        self.title = title
        self.time = datetime.utcnow()
        self.comments = []

    def __repr__(self):
        return '<%s %r>' % (type(self).__name__, self.title)

    def add_comment(self, username, content):
        comment = Comment(username, content, in_reply_to=self)
        self.comments.append(comment)
        return comment


class Comment(object):

    def __init__(self, username, content, in_reply_to=None):
        self.username = username
        self.content = content
        self.in_reply_to = in_reply_to
        self.time = datetime.utcnow()
        self.replies = []

    def __repr__(self):
        return '<%s>' % (type(self).__name__)

    def add_reply(self, username, content):
        reply = Comment(username, content, in_reply_to=self)
        self.replies.append(reply)
        return reply

And import those classes in geddit/controllers.py, just below the other imports:

from geddit.model import Submission, Comment

Now let's add some initial content to the “database”. You'll need to stop the CherryPy server to do that. Then, in the terminal, from the tutorial directory, launch the interactive Python shell, and execute the following code:

>>> from geddit.model import *
>>> data = []
>>> submission = Submission(username='joe', url='http://example.org/', title='An example')
>>> comment = submission.add_comment(username='jack', content='Bla bla bla')
>>> comment.add_reply(username='joe', content='Bla blabla bla bla bla')
>>> data.append(submission)
>>> submission = Submission(username='annie', url='http://reddit.com/', title='The real thing')
>>> data.append(submission)
>>> data
[<Submission 'An example'>, <Submission 'The real thing'>]

>>> import pickle
>>> pickle.dump(data, open('geddit.db', 'wb'))
>>> ^D

You should now have two submissions in the pickle file, with the first submission having a comment, as well as a reply to that comment. Restart the CherryPy server by running:

$ PYTHONPATH=. python geddit/controller.py geddit.db

Now let's change the Root.index() method to pass the submissions list to the template:

    @cherrypy.expose
    def index(self):
        tmpl = loader.load('index.html')
        stream = tmpl.generate(submissions=self.data)
        return stream.render('html', doctype='html')

And finally, we'll modify the index.html template so that it displays the submissions in a simple ordered list. While we're at it, let's add a link to submit new items:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/">
  <head>
    <title>Geddit</title>
  </head>
  <body>
    <h1>Geddit</h1>

    <p><a href="/submit/">Submit new link</a></p>

    <ol py:if="submissions">
      <li py:for="submission in submissions">
        <a href="${submission.url}">${submission.title}</a>
        posted by ${submission.username}
        at ${submission.time.strftime('%M/%d/%Y %H:%m')}
      </li>
    </ol>

  </body>
</html>

When you reload the page in the browser, you should see a page similar to this:

Browser screenshot 1

Adding a Submission Form

In the previous step, we've already added a link to a submission form to the template, but we haven't implemented the logic to handle requests to that link yet.

Attachments (7)

Download all attachments as: .zip