- Your model objects represent the data that your application is working with.
- Your controller pulls in information coming from the user's web browser. It uses that information to update information in the model and to prepare the model for use by the view. The controller also chooses which view to use for the data it returns.
- In TurboGears, your view is in templates that present the information that is provided by the controller.
You have these three areas to populate with code in order to make your application. TurboGears provides help at each point.
CherryPy makes it easy for your controller to get information from the web and string together all of the pieces that comprise your website.
SQLObject makes it easy to define and work with your model. Kid makes it easy to generate good HTML for the browser.
MochiKit makes it easy to implement complex in-the-browser behavior and can even be used to format output in AJAX requests that skip over Kid.
CherryPy Exposed
In CherryPy, your website is represented by a tree of objects and their methods. The way applications are set up in CherryPy, it's easy to follow how a request makes it through your system to reach its destination. How does CherryPy know where to start with a request? It starts a cherrypy.root.
If you look at the gs-start.py script, you'll see this (among other things):
from gs.controllers import Root
cherrypy.root = Root()
By convention, your root controller class is called "Root" and it comes from the "controllers.py" module in your project's package. In a small application, one module of controllers is all you need. In a larger application, you will likely want to break your application up into separate packages. You'd want to use divisions that make sense for you. You can still import the controllers from your various packages into your controllers module, if you'd like them all available in one place.
As long as you make sure that yourpackage.controllers.Root is the root object you want for your webapp, you're all set.
import turbogears
class HardCodedAddition:
def cantgohere(self):
pass
@turbogears.expose()
def twoplustwo(self):
return "four"
@turbogears.expose()
def fortyplustwo(self):
return "forty-two"
class AlwaysSeven:
@turbogears.expose()
def index(self):
return "7"
class Root:
seven = AlwaysSeven()
add = HardCodedAddition()
@turbogears.expose()
def index(self):
return "Welcome"
@turbogears.expose()
def fun(self):
return "...and yet startlingly productive"
@turbogears.expose()
def default(self, *args):
return "The next parts were %s" % str(args)
Let's get a nicely contrived set of classes together to see URL lookup and controller traversal works:
With this setup, we'll be able to easily see how URL traversal works in TurboGears. These same rules apply to CherryPy even without the rest of TurboGears.
URL path |
What you see |
Notes |
/ |
Welcome |
If nothing else is given, the index method is run |
/fun |
...and yet startlingly productive |
If a method matches, it's run directly |
/seven |
Redirect to /seven/ |
Subobjects are treated like directories, so you get redirected with a slash on the end. This is a good thing, because it makes sure that your relative URLs will work. |
/seven/ |
7 |
Just running the index method of the subobject |
/add/cantgohere |
Error! |
This method is not exposed. CherryPy will only navigate to exposed methods. |
/add/twoplustwo |
four |
Just running the method of the subobject |
/foobar |
The next parts were ('foobar') |
If no match is found for the remaining URL parts, the default method is called. |
/foobar/baz/bork |
The next parts were ('foobar', 'baz', 'bork') |
The remaining parts of the URL are split up and sent into the default methods as arguments. |
Between constructing your object tree however you need to and the default method, you have very sophisticated means to determine what happens when a URL is hit.
I wanted an argument!
One of the features that makes CherryPy (and TurboGears by association) so much fun to use is that parameters coming in from a web request are automatically turned into parameters for your method. This makes handling web requests about as natural as possible.
Consider a root that looks like this:
class Root:
@turbogears.expose()
def index(self, name="Arthur"):
return "I am %s, King of the Britons" % name
You probably won't be surprised to learn that access "/" will return "I am Arthur, King of the Britons". Default arguments work exactly as you'd expect them to. In fact, now that you're ready for it, I'm guessing that request parameters will work exactly as you expect them to. A request to "/?name=Lancelot" would return "I am Lancelot, King of the Britons".
Just as in Python itself, calling "/?foobar=baz" will raise an exception because it will try to call root.index(foobar="baz"). And, if you don't know exactly what to expect on the way in, you can always do this
class Root:
@turbogears.expose()
def index(self, name="Arthur", **kw):
return "I am %s, King of the Britons" % name
Just as in standard Python, kw would be a dictionary holding the rest of the request arguments.
Testing, 1... 2... 3...
Test-driven development is a good practice. By writing your tests before the code, you solidify in your mind what the code really needs to do and whether it's structured to do what you want.
Even if you don't write tests first, having automated tests can be a lifesaver when it comes time to change things around later on.
When you install TurboGears, you also get TestGears for free. TestGears is a simple addition to the standard Python unittest module that eliminates the need to manually write test suites.
A separate document will cover testing in detail. Here's a sample controllers.py:
import turbogears
class Root:
@turbogears.expose(html="gs.templates.welcome")
def index(self, value="0"):
value = int(value)
return dict(newvalue=value*2)
TestGears looks for modules that start with test_. By convention, test modules go into separate packages called "tests" located beneath the package they are testing, though TestGears does not require this. Here is a simple test module for the class above:
from turbogears.tests import util
from gs.controllers import Root
import cherrypy
def test_withtemplate():
"Tests the output passed through a template"
cherrypy.root = Root()
util.createRequest("/?value=27")
assert "The new value is 54." in cherrypy.response.body[0]
def test_directcall():
"Tests the output of the method without the template"
d = util.call(cherrypy.root.index, "5")
assert d["newvalue"] == 10
Notice that these tests are just functions, and not unittest.TestCases. You can freely use unittest.TestCases also, if you prefer. Test functions must begin with "test". The docstring provides nicer output in the testrunner.
The template test assumes that the welcome template contains something like: "The new value is ${newvalue}."
To run the tests, you just run:
python setup.py testgears
The template test goes through CherryPy's request handling machinery, so you can test everything from URL traversal to filters to template processing.
Validating and converting arguments
TurboGears provides a simple means to use FormEncode validators to check whether incoming method arguments are valid and to convert them from strings to appropriate Python types. Here's a simple example:
|
1 |
import turbogears |
2 |
from turbogears import validators |
3 |
|
4 |
class Root: |
5 |
|
6 |
@turbogears.expose(html="gs.templates.welcome", |
7 |
validators={"value" : validators.Int()}) |
8 |
def index(self, value=0): |
9 |
return dict(newvalue=value*2) |
10 |
|
11 |
def validation_error(self, funcname, kw, errors): |
12 |
|
13 |
|
|
view plain | print | copy to clipboard | ? |
This dictionary passed to turbogears.expose is saying to run the value argument through the Int validator. You are assured that if the index method is called, value will be an int.
If the validation fails, a method called validation_error (if present) will get called instead of the original target method. The first argument is a string with the name of the method that failed validation. You can use this to pick an appropriate template to render, for example. The second argument is the dictionary of arguments that was passed into the original method. The final argument is a list of Invalid exceptions that were raised.
Invalid exception objects can provide a user-friendly error message. These objects and the original arguments allow you to repopulate an entry form that failed validation.
What should the validation_error method do? It is free to do whatever the original method could do. It can return a dictionary or string. It can raise a cherrypy.HTTPRedirect to take the user to another page. It should be easy to do whatever makes sense for your app.
All of the validators are imported into the turbogears.validators module. You can see the validators that are available by looking at the FormEncode validators module and the TurboGears validators module.
You can do more advanced validation using a FormEncode validation schema. The validators parameter to expose allows you to pass either a schema or a dictionary as in the example above. For more information about schemas, refer to the FormEncode Validator documentation.
How a view template is chosen
There have already been some references to an "html" parameter to turbogears.expose, but now all will be made clear.
As with CherryPy alone, you can return a string and have that string presented to the browser. That's nice but, more often than not, you want to present a whole web page. With TurboGears, you can easily populate a Kid template by returning a dictionary of variables rather than a string, and by passing that "html" parameter to turbogears.expose.
The html parameter gives the full Python package path to get to the kid template. In a quickstarted project, your templates will be in "packagename.templates". The files all end in ".kid", but you can leave that out when specifying the template in the html parameter. So, if you have a template called "welcome", you can say html="packagename.templates.welcome".
What goes into the dictionary? Anything you want the template to have access to. If you return dict(newvalue=5), then ${newvalue} in the template will get the int 5.
Sometimes, in the processing of a request you find that you need a different template than the one you knew about initially. If the dictionary you return has a "tg_template" key, the template you specify there will be used instead. return dict(tg_template="packagename.templates.lancelot") will render the output using the lancelot template, rather than the one in the html parameter.
Returning XML instead of HTML
First and foremost, Kid is an XML template language. With the HTML serializer, it produces good HTML. But, with other serializers, you can also produce XHTML or other XML formats.
The expose function lets you specify a format (which can be "json" or one of Kid's serializers) and a content_type. Let's say you wanted to return RSS, for example. You could set up your expose call like this:
@turbogears.expose(template="project.templates.rss",
format="xml", content_type="text/xml+rss")
The expose function defaults to HTML, but it's easy to change to a different format as needs require. Note that expose lets you pass in the template as the "template" parameter or as the "html" parameter.
Brief introduction to Kid templates
Kid templates can be any XML document with namespaced attributes that tells Kid how to process the template. In practice, your templates will be XHTML documents that will be processed and transformed into valid HTML documents.
This example (straight from Kid's documentation) shows what Kid is like:
|
1 |
<?python |
2 |
title = "A Kid Test Document" |
3 |
fruits = ["apple", "orange", "kiwi", "M&M"] |
4 |
from platform import system |
5 |
?> |
6 |
<html xmlns:py="http://purl.org/kid/ns#"> |
7 |
<head> |
8 |
<title py:content="title">This is replaced.</title> |
9 |
</head> |
10 |
<body> |
11 |
<p>These are some of my favorite fruits:</p> |
12 |
<ul> |
13 |
<li py:for="fruit in fruits"> |
14 |
I like ${fruit}s |
15 |
</li> |
16 |
</ul> |
17 |
<p py:if="system() == 'Linux'"> |
18 |
Good for you! |
19 |
</p> |
20 |
</body> |
21 |
</html> |
22 |
|
|
view plain | print | copy to clipboard | ? |
Important points about Kid templates:
- Don't forget to define the py XML namespace (as is done in the html tag above). That's key to having your template understood as valid XML.
- ${} lets you easily substitute in a Python expression anywhere in your document.
- $foo lets you substitute in foo, but it's not as safe as using the {} because it can be harder to detect where you substitution is supposed to end
- This is XHTML, so you need to close all of your tags. For example, in HTML you'd write <br> to put a line break. In XHTML, you need to write <br/>. This will get converted properly to HTML on the way out.
- Since the template needs to be valid XML, if you have JavaScript with < and >, you should enclose the script in a <![CDATA[]]> section
- There are some reserved words in Kid templates: write, serializer, serialize, generate, initialize, pull, content, transform. If you use those words as template variable names, you'll likely get an error.
One of the great things about Kid is that everything you know about Python applies here. Kid templates get compiled to Python code this makes it easier to predict how things will behave. For example, that py:for="fruit in fruits" behaves just like "for fruit in fruits:" in Python.
The variables that you defined in your dictionary are all available for your use. In any of the "py" attributes, just use the variable from the dictionary as you would a local variable in Python. In the py:for="fruit in fruits" example, "fruits" would be some kind of iterable object passed in via the dictionary.
When variables are dropped in to your template, Kid will automatically escape them. You don't even need to think about running into problems with values that contain <, for example. The time when you do need to care is if you actually have XHTML itself to drop in place. If you do, wrap your substitution value in XML(). For example, let's say we had an XHTML fragment called "header". You could just say ${XML(header)}, and the header would be dropped in without being escaped.
Rather than reproduce it here, a quick read that is well worth it is the reference to Kid's attribute language.
Being Designer-Friendly
Since they are standard XHTML, you can load up Firefox and open your Kid templates directly. If it's important or useful for you, you can make the templates look great all by themselves when loaded in the browser.
What do you need to watch for? Stylesheets, JavaScript and variable substitutions.
Sometimes, it's inconvenient or impossible to have a stylesheet href that works properly when viewing the template directly and after the template is rendered. To get around this, you can use href to handle viewing the template in your browser and py:attrs to handle the rendered page. For example:
When you're viewing the template in your browser, your browser's only going to look at the href attribute, so your stylesheet will be loaded properly. When the template is rendered for the final view, Kid will substitute the value that's in py:attrs, so that the stylesheet works properly on the web. When dealing with JavaScript, this same approach will work for the src attribute on the script tag.
Application-wide headers and footers
Kid offers a number of ways to do application-wide headers and footers. Let's focus on a couple particular approaches: the cool approach and the fast approach.
Kid has a really, really useful command called py:match. What makes it so useful is that you can write individual page templates that know nothing about the site-wide styling that will be applied. The quickstart command starts you off with this kind of setup.
Let's start with a page template that we want to have headers/footers applied to (this is based on gs/templates/welcome.kid):
|
1 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
2 |
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" |
3 |
py:extends="'master.kid'"> |
4 |
|
5 |
<head> |
6 |
<meta content="text/html; charset=UTF-8" http-equiv="content-type" /> |
7 |
<title>Welcome to TurboGears</title> |
8 |
</head> |
9 |
|
10 |
<body> |
11 |
<h2>You're running TurboGears!</h2> |
12 |
|
13 |
<p>Your TurboGears server is running as of <span py:replace="now">now</span>.</p> |
14 |
<div py:replace="phraseOfTheDay()"/> |
15 |
</body> |
16 |
</html> |
|
view plain | print | copy to clipboard | ? |
The only thing in this template that is necessary to get headers and footers applied is the py:extends up in the HTML tag. Kid allows one template to extend or subclass another template. When you do this, your template inherits the parent's py:match and py:def blocks. py:extends works with a string that looks for a file relative to the current template, or a template module object that you have imported.
What does the master template look like?
|
1 |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
2 |
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> |
3 |
|
4 |
<head> |
5 |
<meta content="text/html; charset=UTF-8" http-equiv="content-type" /> |
6 |
<title>Your title goes here</title> |
7 |
</head> |
8 |
|
9 |
<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'"> |
10 |
<h1>TurboGears is Running</h1> |
11 |
|
12 |
<div py:def="phraseOfTheDay()">There's no better time than the present!</div> |
13 |
|
14 |
<p>This text appears in the header provided by master.html.</p> |
15 |
|
16 |
<div py:if="tg_flash" class="flash" py:content="tg_flash"></div> |
17 |
|
18 |
<div py:replace="item[:]"/> |
19 |
</body> |
20 |
</html> |
|
view plain | print | copy to clipboard | ? |
This master template can be viewed in a browser, just as individual page templates can. The py:match attribute on the body looks for the body of the child template (note the use of XML namespaces). In the py:match statement, "item" gives you access to the element at that point in the document. You can look at the tag or the attributes (through dictionary-style lookup on item.attrib) of the element to see if you're looking at one you care about. In this case, we're just going to take over the body element.
When a match is found, the body of the child (individual page) template is replaced by the one in the master template. How do you get your content in there? At the bottom of this master template, there is a div that is replaced by "item[:]". In this particular example, that says to put everything under the body of the page template into this spot in the master template. Usually, you'll want all of the elements, so item[:] is what you'll use.
py:match is not the fastest way to work, but premature optimization is the root of all evil. If you've got a fairly straightforward app without thousands of users, py:match may work just great for you. py:match has to look compare each element with any match expressions you have defined. If you have very large pages and tons of simultaneous users, you may want something faster.
Kid offers a faster approach: template functions. These work just like normal template functions and they are inherited from the master template. There is a simple one in the example above: phraseOfTheDay.
phraseOfTheDay is defined in the master by py:def on a div. In the page template, it shows up as a py:replace in a div. So, the div in the child page template will just get replaced by the div from the master. This lets you include headers, footers, search boxes and the like easily.
Template functions can also take parameters just like normal Python functions. You could, for instance, have a title parameter that gets passed in from the child template in order to format a heading.
Since template functions are normal Python functions and don't affect any other processing of the template, using this styling method is very fast.
Template variables you get for free
All templates that get rendered by TurboGears have access to some basic variables automatically. The variables are defined in the stdvars function in turbogears.view. They provide conveniences for identifying the user's browser, plus common template tasks. See the stdvars documentation for the complete list.
In order to avoid name collisions with your own template variables, all of the predefined variables are found in "std". For example, the user agent is found in std.useragent.
One in particular that's worth noting is std.ipeek, which lets you look at the first item of an iterator without actually consuming it from the iterator. This comes in handy when you have an iterator that might be empty. If it is empty, you may not display a whole block from your template. Here's an example:
|
1 |
<?python items=std.ipeek(shoppingcart) ?> |
2 |
<div py:if="items"> |
3 |
<p>Your cart contains:</p> |
4 |
<ul> |
5 |
<li py:for="item in items" py:content="item">Watermelon</li> |
6 |
</ul> |
7 |
</div> |
8 |
<div py:if="not items"> |
9 |
<p>Your cart is empty</p> |
10 |
</div> |
|
view plain | print | copy to clipboard | ? |
When you have a list, you don't need to use std.ipeek. But, when all you have is an iterator, this is a handy tool.
If you have site-wide variables that you would like available in your templates, you can add a callable to turbogears.view.variableProviders. This will be called with a dictionary that is already set up with the TurboGears standard variables. If you want the current time available in all of your templates, you can just add this to a module:
|
1 |
import time |
2 |
from turbogears import view |
3 |
|
4 |
def addtimevar(variables): |
5 |
variables["currenttime"] = time.ctime() |
6 |
|
7 |
view.variableProviders.append(addtimevar) |
|
view plain | print | copy to clipboard | ? |
Using URLs
CherryPy makes defining what happens for a given URL quite easy: you just give your Python objects the methods you need. In your templates and your code, you can just use the URLs based on how you've defined them for CherryPy.
Sometimes, however, the URLs you use are affected by the environment they're in. For example, if your whole TurboGears site is running at http://yoursite.com/~yourusername/tg, you'll need to be sure that absolute URLs take that into account.
TurboGears provides a convenient url function to create these URLs. Let's say you want to redirect to /view?id=5&page=10. You can call turbogears.url("/view", id=5, page=10) to get a URL that takes into account the top of the site. How does it know where the top of the site is? The server.webpath configuration variable will tell it where the top of your site is.
The url function is also available in templates as "std.url".
Future versions of TurboGears will also make it easy to wire up multiple TurboGears applications in one site. The url function already understands the concept of an application root and will generate absolute URLs relative to the application root. Note: in order for the url function to recognize the application root, your root must extend turbogears.controllers.Root.
JSON output
By returning a dictionary from your controller methods, you make it possible for TurboGears to dynamically decide how the data should be presented to the browser. Most often, you'll want to return HTML.
When doing modern, interactive web applications ("AJAX"), you often want to send data to the browser in a form that is readily usable by JavaScript. While modern browsers all have some level of support for XML, that support varies in its depth and speed. For this reason, the lightweight JSON format is a good one. The format is native to JavaScript, so it works in all browsers and is very fast for the browser to deal with.
If a request comes in with a parameter of "tg_format=json", TurboGears will convert your Python dictionary into the equivalent JSON. If you eval this in the browser, you'll have a hash table of your data, ready to use!
This means that your application can instantly get a usable API in addition to its browser-based interface!
One important caveat as of TurboGears 0.8: only the main Python types are supported by the current JSON implementation. SQLObject-based objects cannot yet be directly serialized as JSON. In the future, there will likely be a way to plug in your own code to take your objects and produce sane JSON versions of them.
Brief introduction to MochiKit
While TurboGears is a Python-based megaframework, a modern webapp has to use JavaScript to provide a good user experience. JavaScript includes some of the conveniences that we, as Python programmers, are used to: built-in dictionaries (hashes) and listish arrays, regular expressions, functions as objects that can be passed around, etc. JavaScript also lacks a number of the tools that we're used to. The basic JavaScript library also does not include everything you'd need for a complete app.
MochiKit fills in a lot of the gaps in a way that is both natural for Python and JavaScript. It is broken down into several packages, but you can have immediate access to all of its functionality by adding one line to your page's <HEAD>:
<script src="${std.tg_js}/MochiKit.js"/>
Your templates automatically get the "std.tg_js" variable, and MochiKit is automatically made available as a static file from the server. When you load MochiKit this way, you're getting everything from the MochiKit modules. MochiKit includes:
- Async — manage asynchronous tasks (including AJAX/XMLHttpRequest)
- Base — functional programming and useful comparisons
- DOM — painless DOM manipulation API
- DateTime — "what time is it anyway?"
- Format — string formatting goes here
- Iter — itertools for JavaScript; iteration made HARD, and then easy
- Logging — we're all tired of alert()
- Visual — visual effects and colors
MochiKit includes so much functionality, that this module breakdown makes it much easier to follow (and work with, if you're making improvements to it).
MochiKit's documentation is very good, so there's no reason to duplicate it here. To get an idea what each module has to offer, take a look at the introductory material at the top of each module's documentation. It is worth skimming through the function reference as well, because each module has features that will make your life easier. For example, you know from the synopsis that MochiKit.Base offers features like map and repr... but did you know that it also has functions for putting together and parsing HTTP query strings?
Defining your model
Your model represents whatever data your application works with. TurboGears models are typically represented using SQLObject objects. SQLObject provides a bridge between Python objects and a relational database. It's not designed to hide away the database 100%, but it does go a long way toward letting you write Python and not worry about SQL.
Before TurboGears can access your database, you need to tell it where to find the database. You do this in the dev.cfg file (for development) or the prod.cfg file (for production use). The config parameter "sqlobject.dburi" controls where the database is stored, and uses a typical URI scheme.
You can also provide options via "query parameters" on the connection URI. A couple of useful options are debug and debugOutput. If you add ?debug=1 to your URI, each query will be output as it is run. If you add &debugOutput=1, you'll also see the result of the query displayed.
You define your data model in the model.py module in your project's package. If you have more model code than will comfortably fit in one module, you can always break it up into multiple modules and import into the model module. SQLObject provides two different ways to define your database: you can define it in SQL directly in your database, or you can define it in Python. For clarity in your code, it is often easier to define your model in Python terms. If you do want your Python classes to be based on your database, you just do this:
Note that this only works with some databases. Check the SQLObject documentation to be certain this style of working.
Defining your model in Python requires more typing of Python code, but saves you from having to write SQL to create your database. Here's an extended book example written in Python code:
|
1 |
class Book(SQLObject): |
2 |
isbn = StringCol(length=13, alternateID=True) |
3 |
title = StringCol(length=100) |
4 |
description = StringCol() |
5 |
authors = RelatedJoin('Author') |
6 |
|
7 |
class Author(SQLObject): |
8 |
last_name = StringCol(length=40) |
9 |
first_name = StringCol(length=40) |
10 |
books = RelatedJoin('Book') |
|
view plain | print | copy to clipboard | ? |
To create this database, you just need to run:
tg-admin sql create
This will create three tables in your database: one for books, one for authors, and a join table for the many-to-many relationship between them.
Though SQLObject cannot model every database, it does well against a wide variety of databases. Many aspects of how the database is used, such as column names, table names and join table names can be customized as needed to fit existing databases.
Your model objects need not (and should not!) be dumb data containers. They are full Python objects and can have methods of their own to perform more complex calculations and operations.
There is quite a bit more information about defining your data model with SQLObject in the SQLObject documentation. Note that as of this writing, the SQLObject documentation is for version 0.6.1 and TurboGears uses version 0.7. THe most user-visible part of the change is that the metadata used by SQLObject in managing your database has moved into a class called sqlmeta. The SQLObject wiki includes more information about using sqlmeta (for things like fromDatabase, cache control, the table name and the id column name).
Using your model
SQLObject makes accessing your database seem very much like using any other Python objects. Behind the scenes, SQL queries are being run as you do things. To insert a new row into your database, just instantiate an object from the appropriate class. Here's an example with debug logging (?debug=1&debugOutput=1) turned on:
>>> Book(isbn="1234567890", title="A Fistful of Yen", description="An evocative look at Japanese currency surrounded by hands.")
1/QueryIns: INSERT INTO book (isbn, description, title) VALUES ('1234567890', 'An evocative look at Japanese currency surrounded by hands.', 'A Fistful of Yen')
1/QueryIns-> 1
1/COMMIT : auto
1/QueryOne: SELECT isbn, title, description FROM book WHERE id = 1
1/QueryR : SELECT isbn, title, description FROM book WHERE id = 1
; 1/QueryOne-> (u'1234567890', u'A Fistful of Yen', 'An evocative look at Japanese currency surrounded by hands.')
1/COMMIT : auto
<Book 1 isbn='1234567890' title='A Fistful of Yen' description="'An evocative loo...'">
Though there are ways to have keys handled differently, SQLObject works best in its default setup where each table has an integer primary key. You can get at any SQLObject's primary key through the "id" attribute. SQLObject makes it really easy to retrieve by ID.
>>> Book.get(1)
<Book 1 isbn='1234567890' title='A Fistful of Yen' description="'An evocative loo...'">
When you specify that a column is an "alternateID", as we did for the "isbn", SQLObject automatically creates a classmethod so that you can use to search on those values:
>>> Book.byIsbn("1234567890")
1/QueryOne: SELECT id, isbn, title, description FROM book WHERE isbn = '1234567890'
1/QueryR : SELECT id, isbn, title, description FROM book WHERE isbn = '1234567890'
1/QueryOne-> (1, u'1234567890', u'A Fistful of Yen', 'An evocative look at Japanese currency surrounded by hands.')
1/COMMIT : auto
<Book 1 isbn='1234567890' title='A Fistful of Yen' description="'An evocative loo...'">
Of course, there are plenty of times when we need to do searches beyond just simple ID lookups. SQLObject provides a "select" classmethod that lets you specify many queries in more Python-like terms. Your class has a special "q" attribute that gives you access to a placeholder for a real attribute to use in queries. For example, to query on the isbn column, you would use Book.q.isbn. Here's a sample query:
>>> list(Book.select(AND(LIKE(Book.q.title, "%Fistful%"), Book.q.isbn=="1234567890")))
1/Select : SELECT book.id, book.isbn, book.title, book.description FROM book WHERE ((book.title LIKE '%Fistful%') AND (book.isbn = '1234567890'))
1/QueryR : SELECT book.id, book.isbn, book.title, book.description FROM book WHERE ((book.title LIKE '%Fistful%') AND (book.isbn = '1234567890'))
1/COMMIT : auto
[<Book 1 isbn='1234567890' title='A Fistful of Yen' description="'An evocative loo...'">]
In the example above, you'll note the call to "list" around the Book.select call. The select classmethod returns a SelectResults object. The neat thing about SelectResults is that until you start pulling data out of it, it's just a placeholder for the results. Rather than converting the results to a list, we could have added .count() to the end of the select call in order to just retrieve the number of matching rows.
Updates are very easy: just change the attribute! Every time you change an attribute, SQLObject will run an UPDATE SQL statement. Sometimes, though, you may need to change several columns at once and don't want to run individual updates for each. Your instances have a "set" method that lets you set them all at once. Here are examples of both styles:
>>> book.title = "A Fistful of Foobar"
1/Query : UPDATE book SET title = 'A Fistful of Foobar' WHERE id = 1
1/QueryR : UPDATE book SET title = 'A Fistful of Foobar' WHERE id = 1
1/COMMIT : auto
>>> book.set(title="A Fistful of Yen 2: Electric Boogaloo", isbn="37")
1/Query : UPDATE book SET isbn = '37', title = 'A Fistful of Yen 2: Electric Boogaloo' WHERE id = 1
1/QueryR : UPDATE book SET isbn = '37', title = 'A Fistful of Yen 2: Electric Boogaloo' WHERE id = 1
1/COMMIT : auto
TurboGears makes it easy to use transactions, via its "connection hub". The connection hub automatically connects to the database as needed, and also gives you methods to begin, commit, rollback or end transactions. Here's an example of transactions at work:
>>> book.title
'A Fistful of Yen 2: Electric Boogaloo'
>>> hub.begin()
>>> book.title = "A Fistful of Yen 3: The Sequel That Shouldn't Be"
1/Query : UPDATE book SET title = 'A Fistful of Yen 3: The Sequel That Shouldn''t Be' WHERE id = 1
1/QueryR : UPDATE book SET title = 'A Fistful of Yen 3: The Sequel That Shouldn''t Be' WHERE id = 1
>>> hub.rollback()
1/ROLLBACK:
>>> hub.end()
>>> book.title
"A Fistful of Yen 3: The Sequel That Shouldn't Be"
>>> book.sync()
1/QueryOne: SELECT isbn, title, description FROM book WHERE id = 1
1/QueryR : SELECT isbn, title, description FROM book WHERE id = 1
1/QueryOne-> (u'37', u'A Fistful of Yen 2: Electric Boogaloo', 'An evocative look at Japanese currency surrounded by hands.')
1/COMMIT : auto
>>> book.title
'A Fistful of Yen 2: Electric Boogaloo'
Notice that, unlike in the previous examples, there was no COMMIT : auto for these queries. That's because we turned on transactions, so autocommit was automatically turned off. You can also specify that you don't want autocommit by adding an autoCommit=0 parameter to your connection URI.
It is also worth noting that the book object that we had in memory did not revert to its database state automatically on rollback. By calling sync(), the values are reloaded from the database.
Once you've had enough of "A Fistful of Yen 2", you can delete it from the database by using the destroySelf method.
Further Reading
SQLObject and many of the other topics touched upon here are too large for a brief introductory guide. Take a look at the documentation page for links to further resources. Documentation suggestions and patches are welcomed.