Pylons Quick Site Development

Author: Dave Kuhlman
Address:
dkuhlman@rexx.com
http://www.rexx.com/~dkuhlman
Revision: 1.0a
Date: Feb. 12, 2007
Copyright: Copyright (c) 2006 Dave Kuhlman. All Rights Reserved. This software is subject to the provisions of the MIT License http://www.opensource.org/licenses/mit-license.php.

Abstract

This document attempts to help you to quickly build your first Pylons Web application.

Contents

1   Introduction

There is good documentation at the Pylons Web site. In particular, you may want to work through the example applications at The Path to Mastery.

2   Installation

Follow the instructions at Installing Pylons.

Hopefully, you can install Pylons with the following:

$ easy_install Pylons

Pylons allows you to choose which of several templating languages is used in your application. The default is Myghty. However, it appears that there is movement toward Mako. Also consider:

If you decide to use a templating language other than the default (currently Myghty), then see Template Language Plugins for instructions.

Also see the Pylons Wiki.

Installing Mako can done with:

$ easy_install Mako

A small word of caution here -- When you use easy_install to install Pylons, a number of different Python packages will be installed on your machine. And, easy_install forces the path to the packages that it installs up higher in sys.path than other packages installed on your system. If you have a specially customized version of one of these packages, your Python will no longer find that version. I had this difficulty with Docutils, which contains an extra output writer ODF/ODT writer for Docutils <http://www.rexx.com/~dkuhlman/odtwriter.html> that is not in the standard Docutils distribution. If this is an issue for you, then you may have to use a sitecustomize.py module or other technique in order to use your version of that module from Python.

3   Creating an Application

  1. Generate the application skeleton and files -- Python Paste does this for us. Paste was installed when you installed Pylons, if it was not already there.

    Let's create an application called FirstApp, which will be the example used throughout this document. Use the following:

    $ paster create --template=pylons FirstApp
    
  2. Edit the configuration file -- FirstApp/development.ini. For example, you may want to change the port on which your application is served.

  3. Start your new application. If you changed the port to 5004, for example, for example, then you should be able to view a default page in your Web browser at http://localhost:5004/.

4   Creating a Controller

Now we will create a controller and map a URL to a method in the conroller, so that when that URL is requested, our method in the controller will be called. The controller tells our application what to do in response to each request. A controller is the module and class that handles requests from your clients.

  1. Create the controller -- We will create a controller using Python Paste. Something like the following should do it:

    $ cd FirstApp
    $ paster controller firstcontroller
    

    This creates a new module firstapp/controllers/firstcontroller.py.

  2. Map a request to the controller -- In order to do this, we will edit the file firstapp/config/routing.py. Add a line like the following:

    map.connect('mapping1', '/firstapp',
        controller='firstcontroller', action='index' )
    

    This maps the URL http://localhost:5004/firstapp to the action/handler index.

    See the Routes documentation and Adding More Complex Mappings below for more on this.

  3. Create the handler (action) in the controller -- Our controller is firstcontroller and our action is index. So, in controllers/firstcontroller.py, we add a method named index. It might look something like this:

    class FirstcontrollerController(BaseController):
        def index(self):
            return Response('<p>firstapp default</p>')
    

Now refresh the Web page http://localhost:5004/firstapp and you should see:

firstapp default

And if it did not work:

5   Creating Content for your Application

To do this, map the URL http://localhost:5004/firstapp/test1 to the action test1. In firstapp/config/routing.py, add:

map.connect('mapping2', '/firstapp/test1',
    controller='firstcontroller', action='test1' )

In firstapp/controllers/firstcontroller.py, add:

def test1(self):
    return render_response('/firstapp/test1.myt')

This tells our application to produce content by calling the action/handler test1 in our controller firstcontroller. And, that test1 method in turn, generates the content by processing the file firstapp/templates/firstapp/test1.myt with Myghty (since we have not changed the default template plugin).

Next, create firstapp/templates/firstapp/test1.myt, containing something like the following:

<html>
<head>
<title>Test #1</title>
</head>
<body>
<h1>Test #1</h1>
<%python>
items = ['one', 'two', 'three', 'four', ]
</%python>
<ol>
% for item in items:
    <li>Item <% item.capitalize() %></li>
% # end for
</ol>
</body>
</html>

The above template contains Myghty code. For the present, it may be enough to understand:

See the Myghty Documentation for help with learning how to write this kind of content.

5.1   Passing values to the template

Although you can include Python code in your Myghty (or other) template, you will often want to place application logic in your controller, and then pass computed values to your template. Here's how. I'll map another URL to a new action as an example.

In firstapp/config/routing.py, add:

map.connect('mapping3', '/firstapp/test2',
    controller='firstcontroller', action='test2' )

In firstapp/controllers/firstcontroller.py, add:

def test2(self):
    random1 = random.randint(1, 10)
    random2 = random.randint(1, 10)
    random3 = random.randint(1, 10)
    random4 = random.randint(1, 10)
    c.random_values = [random1, random2, random3, random4, ]
    return render_response('/firstapp/test2.myt')

Explanation:

  • The variable c -- c is what we use to pass values to our template for one time use. The c variable is reset on each request. See Controller Variables and Template Globals for more explanation.
  • The variable g -- Had we wanted to pass values that would be preserved across multiple requests, we would use the variable g instead. However, the variable g is shared across all requests, which is usually not what you want. See the section on Sessions below for information about how to share values across responses within a single session.
  • The variable h -- h contains helper functions, for example from the Web Helpers package. See section Using Webhelpers to create forms for more on using helpers in forms.

And, now add a template to use those values, specifically, firstapp/templates/firstapp/test2.myt:

<html>
<head>
<title>Test #2</title>
</head>
<body>
<h1>Test #2</h1>
<ol>
% for item in c.random_values:
    <li> <% item %></li>
% # end for
</ol>
</body>
</html>

Explanation:

  • Notice that, using the variable c, we access c.random_values that we set in the controller.

6   Adding More Complex Mappings

The underlying technology used by Pylons to map URLs to actions is Routes. See the Routes Manual.

We have already seen how to add a simple mapping that routes a fixed URL to a specific action (method). In this section we will learn two techniques that make that mapping more flexible.

6.1   Wildcard paths

Here is an example.

  1. First, a mapping in in firstapp/config/routing.py:

    map.connect('mapping4', '/firstapp/test4/*category/help',
        controller='firstcontroller', action='test4')
    
  2. Next, the action/method in firstapp/controllers/firstcontroller.py:

    def test4(self):
        return render_response('/firstapp/test4.myt')
    
  3. And, some trivial content in firstapp/templates/firstapp/test4.myt:

    <html>
    <head>
    <title>Test #4</title>
    </head>
    <body>
    <h1>Test #4</h1>
    <p>Hello.  You want help, right?</p>
    </body>
    </html>
    

Caution: Be aware that problems can occur when mixing wildcard parts with dynamic parts. See Wildcard Limitations and Gotchas for more on this.

6.2   Dynamic paths

A dynamic path is Routes terminology for the ability to assign a part of a path (in a URL) to a variable. Let's consider an example:

  1. First, add the mapping in firstapp/config/routing.py:

    map.connect('mapping3', '/firstapp/test3/:userid',
        controller='firstcontroller', action='test3',
        userid='[nobody]' )
    

    Notice the colon in the path. The value received in that part of the path is assigned, in this case, to the variable userid. Also notice that the variable userid is given a default value "[nobody]".

  2. Next, add the action in firstapp/controllers/firstcontroller.py:

    def test3(self, userid):
        c.userid = userid
        return render_response('/firstapp/test3.myt')
    
  3. And now, add some content in firstapp/templates/firstapp/test3.myt:

    <html>
    <head>
    <title>Test #3</title>
    </head>
    <body>
    <h1>Test #3</h1>
    <p>Hello user <% c.userid %>.</p>
    </body>
    </html>
    

Notice that you can have more than one dynamic part in your URL specification. For example:

map.connect('mapping3', '/firstapp/test3/:userid/:username',
    controller='firstcontroller', action='test3',
    userid='[nobody]', username='[noname]' )

And, we can capture these values in our action with something like the following:

def test3(self, userid, username):
    c.userid = userid
    c.username = username
    return render_response('/firstapp/test3.myt')

7   Add a Form and Retrieve Values From the Form

This section explains two methods of creating HTML forms for Pylons: (1) a simple, manual way and (2) the Web Helpers way.

7.1   Using simple, manual forms

  1. First, we add a mapping to firstapp/config/routing.py:

    map.connect('mapping5', '/firstapp/test5/:itemnumber/:color',
        controller='firstcontroller', action='test5',
        itemnumber='[void]', color='[default]')
    
  2. Next, add an action in firstapp/controllers/firstcontroller.py:

    def test5(self, itemnumber, color):
        if 'itemnumber' in request.params and 'color' in request.params:
            c.itemnumber = request.params['itemnumber']
            c.color = request.params['color']
        else:
            c.itemnumber = itemnumber
            c.color = color
        return render_response('/firstapp/test5.myt')
    

    Our action simply feeds values back to the form. If values have been submitted from the form, the action feeds those back, and if not, it feeds in values (possibly defaults from the mapping) from dynamic parts in the URL.

  3. Now, the content, including the form in firstapp/templates/firstapp/test5.myt:

    <html>
    <head>
    <title>Test #5</title>
    </head>
    <body>
    <form name="test5form" action="/firstapp/test5" method="PUT">
    <h1>Test #5</h1>
    <p>Your current item number is <% c.itemnumber %> and your
    current color is <% c.color %>.
    </p>
    <p>Item number:
    <input type="text" name="itemnumber" value=<% c.itemnumber %> />
    </p>
    <p>Color:
    <input type="text" name="color" value=<% c.color %> />
    </p>
    <p>
    <input type="submit" name="submit" value="submit" />
    </p>
    </form>
    </body>
    </html>
    

A few notes:

  • The action in the form element causes this page, when submitted, to request this page again.
  • Myghty value replacements (for example, value=<% c.itemnumber %>), insert previous values, captured by the controller, into the form.

7.2   Using Webhelpers to create forms

Here we use Webhelpers in creating our form. You can learn more about these and other helpers available in Pylons applications at Web Helpers.

  1. Add the mapping to firstapp/config/routing.py:

    map.connect('mapping6', '/firstapp/test6/:itemnumber/:color',
        controller='firstcontroller', action='test6',
        itemnumber='[void]', color='[default]')
    
  2. Add an action in firstapp/controllers/firstcontroller.py:

    def test6(self, itemnumber, color):
        if 'itemnumber' in request.params and 'color' in request.params:
            c.itemnumber = request.params['itemnumber']
            c.color = request.params['color']
        else:
            c.itemnumber = itemnumber
            c.color = color
        return render_response('/firstapp/test6.myt')
    

    It is almost the same as the previous example.

  3. Add the content in firstapp/templates/firstapp/test6.myt:

    <html>
    <head>
    <title>Test #6</title>
    </head>
    <body>
    <h1>Test #6</h1>
    <p>Your current item number is <% c.itemnumber %> and your
    current color is <% c.color %>.
    </p>
    <% h.form(h.url(action='test6'), method='put') %>
    <fieldset>
    <table border="0" width="50%">
    <tr>
    <td width="50%"><label for="itemnumber">Item number:</label></td>
    <td width="50%"><% h.text_field('itemnumber', value=c.itemnumber) %></td>
    </tr>
    <tr>
    <td width="50%"><label for="color">Color:</label></td>
    <td width="50%"><% h.text_field('color', value=c.color) %></td>
    </tr>
    <tr>
    <td colspan="2" align="center"><% h.submit('Submit') %></td>
    </tr>
    </table>
    </fieldset>
    <% h.end_form() %>
    </form>
    </body>
    </html>
    

Notes:

  • Webhelpers are available in the global varialbe h.
  • We use h.form() and h.end_form() to begin and end our form.
  • Notice the use of h.url() to obtain a URL for our action.
  • In our example, we use h.text_field() to create input text fields and we use h.submit() to create a submit button. Webhelpers also provides functions for the creation of selection boxes, text areas, hidden fields, radio buttons, and check boxes. See Webhelpers Form Tag Helpers.

8   Database Access -- Using a Data Model

We'll use SQLAlchemy, since that seems more in the future. For more information see The Python SQL Toolkit and Object Relational Mapper.

8.1   Configure the use of SQLAlchemy

Uncomment and edit the SQLAlchemy line in FirstApp/development.ini. In my case, I have:

sqlalchemy.dburi = postgres://postgres:xxxx@localhost:5432/test
sqlalchemy.echo_queries = true

Which specifies:

  • User: postgres
  • Password: xxxx
  • Host: localhost
  • Port: 5432
  • Database name: test

Now add the following to firstapp/lib/app_globals.py:

from firstapp.models import init_model

And, add the following to the __init__ method, also in firstapp/lib/app_globals.py:

init_model(app_conf)

This will allow us to make use of app_conf in models/__init__.py in order to get the value for dsn that we defined in development.ini.

8.2   Define and implement the model

I'm using PostgreSQL on my machine. Here is what my test database looks like, as shown using the PostgreSQL utility psql:

test=# select * from plant_db;
   p_name   |      p_desc      | p_rating
------------+------------------+----------
 lemon      | yello and tart   |        5
 sunflower  | brite yellow     |        3
 tangerine  | orange and sweet |        7
 grapefruit | pink and sweet   |        7
(4 rows)

So, in FirstApp/firstapp/lib/app_globals.py we add:

from firstapp.models import init_model

near the top. And, in the same file, we add:

init_model(app_conf)

in the __init__() method.

Next, we add the following to FirstApp/firstapp/models/__init__.py:

import sqlalchemy as sa
import sqlalchemy.orm as orm

class Plant(object):
    def __repr__(self):
        return "%s(%r,%r,%r)" % (
            self.__class__.__name__,
            self.p_name,
            self.p_desc,
            self.p_rating,
            )

def init_model(app_conf):
    global metadata, plant_table
    if not globals().get('metadata'):
        metadata = sa.BoundMetaData(DSN)
        plant_table = sa.Table('plant_db', metadata, autoload=True)
        orm.mapper(Plant, plant_table)

def create_all(app_conf):
    init_model(app_conf)
    metadata.create_all()

8.3   Map the URL

To test all this, we'll add a new mapping for a new URL, as well as a new action and Web page (template).

Add the following to firstapp/config/routing.py:

map.connect('mapping7', '/firstapp/test7/:p_name',
    controller='firstcontroller', action='test6',
    p_name='weed')

The :p_name dynamic part of the URL will allow us to pass in a plant name.

8.4   Implement the action

The action (method) is where we will do the database work, for example, the database queries, the Add operation, the Delete operation, and the Update operation.

As a sample, add something like the following to firstapp/controllers/firstcontroller.py:

import sqlalchemy as sa
import sqlalchemy.orm as orm
from firstapp.models import Plant                    # Note 1 (see below)

class FirstcontrollerController(BaseController):
    o
    o
    o
def test7(self, p_name):
    if ('p_name' in request.params and               # Note 2
        'p_desc' in request.params and
        'p_rating' in request.params):
        c.p_name = request.params['p_name']
        c.p_desc = request.params['p_desc']
        c.p_rating = request.params['p_rating']
    else:
        c.p_name = p_name
        c.p_desc = '[void]'
        c.p_rating = '[void]'
    dbsession = orm.create_session()                 # Note 3
    if 'commit' in request.params:
        if request.params['commit'] == 'Add':        # Note 4
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is None:
                plant = Plant()
                plant.p_name = c.p_name
                plant.p_desc = c.p_desc
                plant.p_rating = c.p_rating
                dbsession.save(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Delete':   # Note 5
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is not None:
                dbsession.delete(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Update':   # Note 6
            query = dbsession.query(Plant)
            plant = query.get_by(p_name=c.p_name)
            if plant is not None:
                plant.p_desc = c.p_desc
                plant.p_rating = c.p_rating
                dbsession.update(plant)
                dbsession.flush()
        elif request.params['commit'] == 'Show':
            pass
    query = dbsession.query(Plant)                   # Note 7
    c.plants = query.execute('select * from plant_db order by p_name')
    dbsession.close()
    return render_response('/firstapp/test7.myt')

Notes:

  1. We import what we need from sqlalchemy and we import our model class.
  2. Next, we capture the variable values from the command line or from the form.
  3. We create a database session. Note that the database connection and the mapper have already been initialized in FirstApp/firstapp/models/__init__.py (see above).
  4. The Add operation -- If a plant with that name does not exist in the database, then create a new plant object, populate its values, save it, and flush the database.
  5. The Delete operation -- Create a query object, then get the plant to be deleted. Next, if the object/plant exists, delete the object and flush to the database.
  6. The Update operation -- Create a query object and get the existing plant. If it exists, update the values in the plant object, then update and flush to the database.
  7. Query the database and pass the values for all plants found into the form. Then, close the database session, and, finally, render the form.

8.5   Implement a page that uses the DB model

Here is a page (using the Myghty templating language) that displays the results of the database query and enables the user to add, delete, and update rows:

<html>
<head>
<title>Test #7</title>
</head>
<body>
<h1>Test #7 -- Plants Database</h1>
<p>Your current item number is <% c.itemnumber %> and your
current color is <% c.color %>.
</p>

<!-- Show the existing database.
-->
<table border="1" width="100%">
<tr>
<th>Name</th>
<th>Description</th>
<th>Rating</th>
</tr>

% for plant in c.plants:
  <tr>
    <td width="20%"><% plant.p_name %></td>
    <td width="60%"><% plant.p_desc %></td>
    <td width="20%"><% plant.p_rating %></td>
  </tr>
% # end for
</table>

<!-- A form for database entries.
-->
<% h.form(h.url(action='test7'), method='put') %>
<fieldset>
<table border="1" width="50%">
<tr>
<td width="50%"><label for="p_name">Plant name:</label></td>
<td width="50%"><% h.text_field('p_name', value=c.p_name) %></td>
</tr>
<tr>
<td width="50%"><label for="p_desc">Description:</label></td>
<td width="50%"><% h.text_field('p_desc', value=c.p_desc) %></td>
</tr>
<tr>
<td width="50%"><label for="p_rating">Rating:</label></td>
<td width="50%"><% h.text_field('p_rating', value=c.p_rating) %></td>
</tr>
</table>
<table border="1" width="50%">
<tr>
<td align="center"><% h.submit('Add') %></td>
<td align="center"><% h.submit('Delete') %></td>
<td align="center"><% h.submit('Update') %></td>
<td align="center"><% h.submit('Show') %></td>
</tr>
</table>
</fieldset>
<% h.end_form() %>
</form>
</body>
</html>

9   Application Structure

This section attempts to give some help with structuring and organizing the code in your application. It's a "what goes where and why" sort of section.

The first thing to notice is that Pylons already gives quite a bit of guidance about structure. For example:

What follows describes a few options and variations.

  1. All in one controller -- Put all your code in one class in a single controller. Consider implementing extended behavior in helper methods (optionally named with a single leading underscore, which the Python style guide says is the convention for non-public members) or in functions outside the controller class.

  2. One controller plus import -- Use a single controller, but put application logic in separate modules. Then, in your controller, import those modules and use the application code that is in classes or functions in those application modules. Consider putting those additional modules in myapp/lib or myapp/controllers or in a subdirectory that you create under one of those. If you create a subdirectory, be sure to add an __init__.py file in the subdirectory.

  3. Multiple controllers -- Use Python Paste to generate multiple controllers, then use option 1 (all in one controller) or option 2 (one controller plus import) above in each of those modules.

  4. If you need variables, functions, or classes for use in multiple templates, you can put definitions in lib/helpers.py. Those definitions will be available in your templates in the h variable. See the comment in lib/helpers.py, which reads as follows:

    "All names available in this module will be available under the Pylons h object."

  5. If you need global variables that live across multiple request/responses, you can assign values to the variable g. You can also initialize the variables in myapp/lib/app_globals.py.

10   Sessions

Sessions in Pylons are reasonably easy. This section presents a quick and trivial example. The example initializes and increments a counter each time this page is visited. If you visit this page from two separate browsers or restart your browser, you should see two separate counts.

Documentation on the use of sessions in Pylons is available under Using Sessions.

For our example, we add the following to firstapp/config/routing.py:

map.connect('mapping8', '/firstapp/test8',
    controller='firstcontroller', action='test8')

And, add an action handler something like the following to your controller, in our case controllers/firstcontroller.py:

def test8(self):
    if 'count' in session:
        count = session['count']
    else:
        count = 0
    count += 1
    session['count'] = count
    session.save()
    c.count = count
    response = render_response('/firstapp/test8.myt')
    return response

Then implement a template to show our counter and how it is incremented. Here is a sample templates/firstapp/test8.myt:

<html>
<head>
<title>Test #8</title>
</head>
<body>
<p>Hello, user.</p>
<p>Count: <% c.count %></p>
</body>
</html>

A few additional notes on sessions:

11   A Few Additional Notes

11.1   Debugging

When your breaks (throws an exception), you will see a traceback in your Web browser window.

Here are a few things that you can do when you receive a traceback:

  • Click on the full traceback button to get more context and a deeper stack trace.
  • Click on the text version button to get the traceback formatted in the standard Python way.
  • Click on the >> to see surrounding statements at that level.
  • Click on the + sign to get an interactive window where you can type in Python commands which will be executed in the context at that level.

Notice that you can use something like the following to break your application at any point and get a traceback:

raise RuntimeError, 'breaking at ...'

11.1.1   Using the Python debugger pdb

Try adding the following in your controller/action:

import pdb; pdb.set_trace()

Type "help" at the debugger prompt to see a list of commands, then type "help" followed by a command name for documentation on that command. Return to and continue on responding to the request by pressing "c".

Learn more about pdb, the Python debugger at The Python Debugger.

11.1.2   Using an IPython embedded shell

IPython provides a more powerfull interactive prompt and a powerful embedded shell. If you are a Python programmer and have not yet tried IPython, you definitely should look into it.

First, import from IPython -- Add something like the following at the top of your controller module, in our case in firstapp/controllers/firstcontroller.py:

from IPython.Shell import IPShellEmbed
args = ['-pdb', '-pi1', 'In <\\#>: ', '-pi2', '   .\\D.: ',
    '-po', 'Out<\\#>: ', '-nosep']
ipshell = IPShellEmbed(args,
    banner = 'Entering IPython.  Press Ctrl-D to exit.',
    exit_msg = 'Leaving Interpreter, back to Pylons.')

Then, place this code in your action/method:

ipshell('We are at action abc')

Return to Pylons and continue on responding to the request by pressing Ctrl-D.

Note that because of some idiosyncratic feature of IPython.Shell.IPShellEmbed, I had to put the following before each call to ipshell():

ipshell.IP.exit_now = False
ipshell('We are at action abc')

Learn more about IPython at IPython: an Enhanced Python Shell.

11.2   Production mode

When you are ready to go live, you will want to do (at least) two things:

  • Uncomment the following line in development.ini:

    set debug = false
    
  • Omit the --reload option when you use Python Paste start your server. In other words, use:

    $ paster serve development.ini
    

    rather than:

    $ paster serve --reload development.ini