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:
What just happened?
TinCan found the
hello.pspx
file.In that file, it found no special header directives. It also found no
hello.py
file.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 inhello.pspx
with it.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:
What new happened this time?
You created a code-behind file with the same name as its associated template (only the extensions differ).
Because the file names match, TinCan deduced that the two files were related, one containing the code-behind for the associated template.
TinCan looked in the code-behind file for a something subclassing
tincan.Page
, and created an instance of that class, complete with the standardrequest
andresponse
instance variables.It then called the
handle(self)
method of that class, which defined yet another instance variable.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:
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.