Quixote: a Python-Centric Web Application Framework

If you need to create dynamic web sites and don't want to learn the syntax and arbitrary limitations of yet another templating language, you should give Quixote a serious look.
From Python to PTL

Let's take a look at the code in the splat.web package. Every namespace (package or module) that Quixote uses must supply two special identifiers: _q_index() and _q_exports. We'll get to _q_exports momentarily. For now, let's concentrate on _q_index(); for the splat.web package, it is supplied through an import in splat/web/__init__.py:

from splat.web.index import _q_index

This fits with the recommended practice of putting as little code as possible in __init__.py files. Any functions or classes that need to be supplied by a package itself, as opposed to a module in that package, should simply be imported in the package's __init__.py.

Quixote' _q_index() is equivalent to index.html, but instead of a file containing the default web page for a directory, _q_index() is a Python callable (e.g., a function, a method or a PTL template) that returns the default web page for a namespace. In fact, there are many useful analogies between a traditional filesystem-based web server (such as Apache) and Quixote's Python-centric way of building a web site.:

Filesystem (e.g. Apache)

Quixote

directory

Python namespace (module, package, ...)

file

Python callable object (function, method, ...)

index.html

_q_index()

file exists, is readable

callable object exists, is in _q_exports

Let's consider a simple _q_index() for SPLAT!, written as a Python function:

from quixote.html import html_quote
from splat.web.util import get_bug_database
def _q_index (request):
    result = ["""\
        <html>
        <head><title>SPLAT! Bug Index</title></head>
        <body>
        <table>
          <tr>
            <th>bug id</th>
            <th>description</th>
          </tr>
        """]
    bug_db = get_bug_database()
    for bug in bug_db.get_all_bugs():
        if bug.status != bug.ST_RESOLVED:
            result.append("""\
                <tr>
                  <td>%s</td>
                  <td>%s</td>
                </tr>
                """ % (bug, html_quote(bug.description))
    result.append("""\
        </table>
        </body>
        </html>
        """)
    return "".join(result)

We build up the web page as a list of strings, which are concatenated at the end. (This is much more efficient that repeated string concatenation border. In fact, a loop of result += ... probably qualifies as an antipattern in Python because of its quadratic running time.)

For this simple example, writing _q_index() as a Python function isn't too inconvenient, but there is a better way: PTL. PTL is simply Python with a different way of specifying function return values. In fact, the above function is quite easy to rewrite as a PTL template:

template _q_index (request):
    """\
    <html>
    <head><title>SPLAT! Bug Index</title></head>
    <body>
    <table>
      <tr>
        <th>bug id</th>
        <th>description</th>
      </tr>
    """
    bug_db = get_bug_database()
    for bug in bug_db.get_all_bugs():
        if bug.status != bug.ST_RESOLVED:
            """\
              <tr>
                <td>%s</td>
                <td>%s</td>
              </tr>
            """ % (bug, html_quote(bug.description))
    """\
    </table>
    </body>
    </html>
    """

At this stage, the differences are unremarkable: instead of explicitly accumulating and returning the HTML document, the PTL version does so implicitly. PTL works by accumulating the result of every statement that runs in a template; each non-None result is stored in an instance of TemplateIO (a class provided by Quixote). When the template returns, it actually returns str() of the TemplateIO object. This converts all of the accumulated statement results to strings (again, with str()) and returns the concatenation of those strings.

PTL starts to get fun when you realize that you can refactor PTL templates just like you can Python functions. For example, we might break up our _q_index() template as follows:

template header (title):
    """\
    <html>
    <head><title>SPLAT! - %s</title></head>
    <body>
    """ % html_quote(title)
template footer ():
    """\
    </table>
    </body>
    </html>
    """
template bug_row (bug):
    """\
      <tr>
        <td>%s</td>
        <td>%s</td>
      </tr>
    """ % (bug, html_quote(bug.description)
template _q_index (request, bug):
    header("Bug Index")
    """\
    <table>
      <tr>
        <th>bug id</th>
        <th>description</th>
      </tr>
    """
    bug_db = get_bug_database()
    for bug in bug_db.get_all_bugs():
        if bug.status != bug.ST_RESOLVED:
            bug_row(bug)
    "</table>\"
    footer()

Now we have reusable header() and footer() templates, and we have simplified the main loop of _q_index(). Any programmer recognizes the value of procedural abstraction; most web-templating languages, unfortunately, do not.

Writing a _q_index() function for our root namespace means Quixote can generate a response when a user requests the base URL corresponding to this application. For example, you might set things up so that /bugs/ is the base URL for SPLAT! at your site, i.e., /bugs/ corresponds to the splat.web package. When your server receives a request for /bugs/, then Quixote will call splat.web._q_index()--which, thanks to that import in splat/web/__init__.py, is really splat.web.index._q_index()--and return the resulting HTML page. But, as long as you can implement something in Python (or PTL), you can use Quixote to associate it with a URL and put it on the web.

______________________

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

quixote wiki link

charter97's picture

welcome to quixote.ca