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.
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.
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.
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
Edit the configuration file -- FirstApp/development.ini. For example, you may want to change the port on which your application is served.
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/.
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.
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.
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.
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:
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.
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:
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:
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.
Here is an example.
First, a mapping in in firstapp/config/routing.py:
map.connect('mapping4', '/firstapp/test4/*category/help', controller='firstcontroller', action='test4')
Next, the action/method in firstapp/controllers/firstcontroller.py:
def test4(self): return render_response('/firstapp/test4.myt')
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.
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:
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]".
Next, add the action in firstapp/controllers/firstcontroller.py:
def test3(self, userid): c.userid = userid return render_response('/firstapp/test3.myt')
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')
This section explains two methods of creating HTML forms for Pylons: (1) a simple, manual way and (2) the Web Helpers way.
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]')
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.
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:
Here we use Webhelpers in creating our form. You can learn more about these and other helpers available in Pylons applications at Web Helpers.
Add the mapping to firstapp/config/routing.py:
map.connect('mapping6', '/firstapp/test6/:itemnumber/:color', controller='firstcontroller', action='test6', itemnumber='[void]', color='[default]')
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.
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:
We'll use SQLAlchemy, since that seems more in the future. For more information see The Python SQL Toolkit and Object Relational Mapper.
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:
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.
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()
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.
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:
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>
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:
routing goes in myapp/config/routing.py.
Actions go in myapp/controllers/mycontroller.py where "mycontroller" is the argument you passed to paster. For example:
$ paster controller mycontroller
Templates go under myapp/templates.
What follows describes a few options and variations.
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.
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.
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.
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."
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.
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:
The session object is available in your controller. You imported it with:
from firstapp.lib.base import *
Treat the session as a dictionary. For example, use:
'mykey' in session
or:
session.has_key('mykey')
to check for the existence of a key.
Use, for example:
if 'address' in session: address = session['address']
and:
session['address'] = address
to get and set a value in the session.
Be sure to save the session before rendering your page:
session.save()
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:
Notice that you can use something like the following to break your application at any point and get a traceback:
raise RuntimeError, 'breaking at ...'
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.
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.
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