Tutorial

First Step: Create a Directory for Your Webapp

TinCan is a lot like a web server; it serves a directory of files. There is one catch, though: that directory must contain a WEB-INF subdirectory. It’s OK if WEB-INF ends up being empty (as it can, for a really simple webapp). If WEB-INF is missing, TinCan will conclude the directory doesn’t contain a webapp and will refuse to serve it. (This is a deliberate feature, added to prevent serving directories that don’t contain webapps.)

So the first thing we must do is create a directory to hold our new webapp. Let’s call it demo:

$ mkdir demo demo/WEB-INF

Adding Some Content

Use your favorite editor to create a file called hello.pspx, and insert the following content into it:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello, World</h1>
    <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
  </body>
</html>

Save the file to disk, and start up a test server:

$ launch demo

You should see something like the following:

adding route: /hello.pspx (GET)
Bottle v0.12.16 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

If you already have a server listening on port 8080, this naturally won’t work. Use the --port option to tell launch to listen on another port, e.g.:

$ launch --port=8000

When you have a working test server, point your browser at http://localhost:8080/hello.pspx and you should see something like the following:

_images/hello1.png

What just happened?

  1. TinCan found the hello.pspx file.

  2. In that file, it found no special header directives. It also found no hello.py file.

  3. So TinCan fell back to its default behavior, and created an instance of the tincan.Page class (the base class of all code-behind logic), and associated the template code in hello.pspx with it.

  4. The ${page.request.environ['PATH_INFO']} expression references objects found in that standard page object to return the part of the HTTP request path that is being interpreted as a route within the webapp itself.

Adding Some Content of Our Own

Use Control-C to force the test server to quit, then add two more files. First, a template, hello2.pspx:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello Again, World</h1>
    <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
    <p>The current time on the server is ${time}.</p>
  </body>
</html>

Next, some code-behind in hello2.py:

import time
import tincan

class Hello2(tincan.Page):
    def handle(self):
        self.time = time.ctime()

This time, when you launch the test server, you should notice a second route being created:

$ ./launch demo
adding route: /hello.pspx (GET)
adding route: /hello2.pspx (GET)
Bottle v0.12.16 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

When you visit the new page, you should see something like this:

_images/hello2.png

What new happened this time?

  1. You created a code-behind file with the same name as its associated template (only the extensions differ).

  2. Because the file names match, TinCan deduced that the two files were related, one containing the code-behind for the associated template.

  3. TinCan looked in the code-behind file for a something subclassing tincan.Page, and created an instance of that class, complete with the standard request and response instance variables.

  4. It then called the handle(self) method of that class, which defined yet another instance variable.

  5. Because that instance variable did not start with an underscore, TinCan considered it to be exportable, and exported it as a template variable.

Suppose you had written the following code-behind instead:

from time import ctime
from tincan import Page

class Hello2(Page):
    def handle(self):
        self.time = ctime()

Every class is a subclass of itself, so what stops TinCan from getting all confused now that there are two identifiers in this module, Page and Hello2, both of which are subclasses of tincan.Page? For that matter, what if you had defined your own subclass of tincan.Page and subclassed it further, e.g.:

from time import ctime
from mymodule import MyPage

class Hello2(MyPage):
    def handle(self):
        self.time = ctime()

The answer is that the code would still have worked (you might want to try the first example above just to prove it). When deciding what class to use, TinCan looks for the deepest subclass of tincan.Page it can find in the code-behind. So in a code-behind file that contains both a reference to tincan.Page itself, and a subclass of tincan.Page, the subclass will always “win.” Likewise, a subclass of a subclass will always “win” over a direct subclass of the parent class.

A code-behind file need not share the same file name as its associated template. If you use the #python header directive, you can tell TinCan exactly which code-behind file to use. For example:

#python other.py
<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello Again, World</h1>
    <p>This page was called on the route ${page.request.environ['PATH_INFO']}.</p>
    <p>The current time on the server is ${time}.</p>
  </body>
</html>

What happens if you edit hello2.pspx to look like the above but do not rename hello2.py to other.py? What happens if you make hello2.pspx run with hello.py (and vice versa)? Try it and see!

(Note that it is generally not a good idea to gratuitously name things inconsistently, as it makes it hard for other people — or even you, at a later time — to easily figure out what is going on.)

Error Pages

If you tried the exercises in the paragraph immediately above, some of them let you see what happens when something goes wrong in TinCan. What if that standard error page is not to your liking, and you want to display something customized? TinCan lets you do that. Create a file called 500.pspx:

#errors 500
<!DOCTYPE html>
<html>
  <head>
    <title>${error.status_line}</title>
  </head>
  <body>
    <h1>${error.status_line}</h1>
    <p>How embarrassing! It seems there was an internal error serving
    <kbd>${request.url}</kbd></p>
  </body>
</html>

Now when you visit a page that doesn’t work due to server-side errors (such as a template referencing an undefined variable), you’ll see something like:

_images/500.png

Error pages work a little differently from normal pages. They are based on subclasses of tincan.ErrorPage. You get two standard template variables “for free:” error, containing an instance of a bottle.HTTPError object pertaining to the error, and request, a bottle.HTTPRequest object pertaining to the request that triggered the error.

It is not as common for error pages to have code-behind as it is for normal, non-error pages, but it is possible and allowed. As alluded to above, the class to subclass is tincan.ErrorPage; such classes have a handle() method which may define instance variables which get exported to the associated template using the same basic rules as for normal pages.

If an error page itself causes an error, TinCan (the underlying Bottle framework, actually) will ignore it and display a fallback error page. This is to avoid triggering an infinite loop.

Index Pages and Server-Side Forwards

Remove any #python header directives you added to hello.pspx and hello2.pspx during your previous experiments and create index.pspx:

#forward hello.pspx

That’s it, just one line! When you launch the modified webapp, you should notice a couple new routes being created:

$ launch demo
adding route: /hello.pspx (GET)
adding route: /hello2.pspx (GET)
adding route: /index.pspx (GET)
adding route: / (GET)
Bottle v0.12.16 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

And if you navigate to http://localhost:8080/, you should see our old friend the first page we created in this tutorial.

Just like how a web server recognizes index.html as special, and routes requests for its containing directory to it, TinCan recognizes index.pspx as special.

#forward creates what is known as a server-side forward. Unlike with an HTTP forward, all the action happens on the server side. The client never sees any intermediate 3xx response and never has to make a second HTTP request to resolve the URL. Any TinCan template file containing a #forward header directive will have everything else in it ignored, and otherwise act as if it were an exact copy of the route referenced in the #forward header.

It is permitted for a page to #forward to another #forward page (but you probably should think twice before doing this). #forward loops are not permitted and attempts to create them will be rejected and cause an error message at launch time.

Form Data and Requests Other Than GET

As a final example, here’s a sample page, name.pspx, that processes form data via a POST request, and which uses some Chameleon TAL to render and style the response page, call it:

#rem A page that accepts both GET and POST requests
#methods GET POST
<!DOCTYPE html>
<html>
  <head>
    <title>Form Test</title>
  </head>
  <body>
    <h1>Form Test</h1>
    <h2>Enter Your Name Below</h2>
    <form method="post">
      <input type="text" name="name"/>
      <input type="submit" value="Submit"/>
    </form>
    <if tal:condition="message is not None" tal:omit-tag="True">
      <h2 tal:content="message.subject"></h2>
      <p tal:attributes="style message.style" tal:content="message.body"></p>
    </if>
  </body>
</html>

Here’s the code-behind for that page:

from tincan import Page
from jsdict import JSDict

class Name(Page):
    def handle(self):
        self._error_message = JSDict({"subject": "Error", "style": "color: red;"})
        if "name" not in self.request.forms:
            if self.request.method == "GET":
                self.message = None
            else:
                self.message = self.error("This should not happen!")
        else:
            name = self.request.forms["name"]
            if name.strip() == "":
                self.message = self.error("Please enter your name above.")
            else:
                self.message = JSDict({
                    "subject": "Hello, {0}".format(name.split()[0]),
                    "style": None,
                    "body": "Pleased to meet you!"
                })

    def error(self, message):
        self._error_message.body = message
        return self._error_message

This example requires you to create a third file, in WEB-INF/lib/jsdict.py:

class JSDict(dict):
    def __getattr__(self, name):
        return self[name]

    def __setattr__(self, name, value):
        self[name] = value

    def __delattr__(self, name):
        del self[name]

WEB-INF/lib is the directory where webapp-specific library routines live. It gets added to sys.path automatically when your webapp is launched.

This example introduces two new header directives, #rem and #methods. The former introduces a remark (comment); the latter specifies the request methods that this page will respond to. Not specifying a #methods line is equivalent to specifying #methods GET. When you launch the webapp after adding this new page, you shound notice TinCan announce that it is creating a route that supports both GET and POST requests for it:

$ launch demo
adding route: /hello.pspx (GET)
adding route: /hello2.pspx (GET)
adding route: /index.pspx (GET)
adding route: / (GET)
adding route: /name.pspx (GET,POST)
Bottle v0.12.16 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.