Python for the web with Webpy

24 Apr 2012

About two weeks ago I discovered the Webpy framework. I really enjoy using it and at the moment I prefer it over Django. The framework is relatively lightweight and allows you to do write more traditional Python than with Django (in my opinion). Webpy is by Aaron Schwartz, who was one of the developers of Reddit, it was used in the first version of Reddit. So in this post I will introduce you to webpy.

Hello World

First of all get the webpy package here. Just follow the steps to install it. To understand how Webpy works, we will write our first hello world program. So create a file (hello.py) with these contents:

#!/usr/bin/env python
import web
urls = ('/','root')
app = web.application(urls,globals())
class root:
    def __init__(self):
        self.hello = "hello world"
    def GET(self):
        return self.hello
if __name__ == "__main__":
        app.run()

Let's break it down:

  • First of all the shebang line (very important!)
  • Line 2 imports the framework
  • Line 3 sets when requesting a url which class should act upon the url (more about this later)
  • Line 4 this line passes the urls to our framework for when we launch our application
  • Line 6 this is a class we define that will need to do something when visiting a certain url
  • Line 8 this is just our constructor
  • Line 11 this method get's called when a GET is passed to our webserver
  • The last lines are just defining a main class to deploy our application
Now to test this application, webpy comes with it's own built in webserver. So just run:

chmod +x hello.py
./hello.py

It will start running on localhost:8080. You should get this returned:

hello world
Now if we want to add a GET parameter so we can say Hello "insert your name" we will need to add two new lines in our GET method.

    def GET(self):
        getInput = web.input(name="World")
        return "Hello "+str(getInput.name)

We get the input from web, if name is not set we will assign a default value called "World". Now if you visit http://localhost:8080/?name=Tux you will get:

Hello Tux
Using GET parameters with "?name= " like patterns is called a query.

Capturing pages

Let's extend the application:

#!/usr/bin/env python
import web
urls = ('/','root','/(.+)', 'capture')
app = web.application(urls,globals())
class root:
    def __init__(self):
        self.hello = "hello world"
    def GET(self):
        getInput = web.input(name="World")
        return "Hello "+str(getInput.name)
class capture:
    def GET(self,name):
        return "You tried %s" % name
if __name__ == "__main__":
        app.run()

We added another class called capture and changed the urls to point at it with a regex

'/(.+)', 'capture'

. This regex that captures every sign that comes after '/'. Every sign that comes after '/' is considered a parameter for the GET method. So we need to pass this parameter to our GET method.

def GET(self,name):

All these parameters are a string object. Run the application and visit http://localhost:8080/Hello/World/This/is/Me You will get:

You tried Hello/World/This/is/Me

You could use this as a catch all. However do note:

#!/usr/bin/env python
import web
urls = ('/','root','/anotherpage','anotherpage','/(.+)', 'capture')
app = web.application(urls,globals())
class root:
    def __init__(self):
        self.hello = "hello world"
    def GET(self):
        getInput = web.input(name="World")
        return "Hello "+str(getInput.name)
class capture:
    def GET(self,name):
        return "You tried "+str(name)
class anotherpage:
    def GET(self):
        return "This is just another page"
if __name__ == "__main__":
        app.run()

If we run this application and go to http://localhost:8080/anotherpage . We will get

Just another page
. If we swap anotherpage and the catch all like this:

urls = ('/','root','/(.+)', 'capture','/anotherpage','anotherpage')

The catch all will be matched first and you will you get:

You tried anotherpage
So do keep an eye on the order of your url matches.

Creating HTML templates

The language to create templates with in webpy is called Templetor. To create a template create a folder that will contain these templates (to keep it tidy). I use a folder called "templates" (it's a bit obvious, I know). Create a file called "mytemplate.html" in the template folder that contains: [html] $def with (title,name) <!DOCTYPE html> $title

Hello $name

[/html] First of all we declare what variables we will use. In our HTML we can call these variables by putting a "$" infront of the variable. Should you want to write javascript, you will need to escape the dollar sign by just adding another dollar sign. So if you have:

$function(){
    //SOMETHING
}

you will need to rewrite it like this:

$$function(){
    //SOMETHING
}

Now let's extend our app so it can make use of the template:

#!/usr/bin/env python
import web
urls = ('/','root',"/anotherpage","anotherpage","/template","templatepage",'/(.+)', 'capture')
app = web.application(urls,globals())
class root:
    def __init__(self):
        self.hello = "hello world"
    def GET(self):
        getInput = web.input(name="World")
        return "AHello "+str(getInput.name)
class capture:
    def GET(self,name):
        return "You tried "+str(name)
class anotherpage:
    def GET(self):
        return "This is just another page"
class templatepage:
    def __init__(self):
        self.render = web.template.render('templates/')
    def GET(self):
        getInput=web.input(name="World")
        return self.render.mytemplate("mytitle",getInput.name)
if __name__ == "__main__":
        app.run()

So we add a new url for "/template" and then we add another class called "templatepage". In the constructor of this class we create a render object. It will look for templates in the "templates/" folder. In the GET method we use the template by using "self.render.". You can also write python directly into your template, for instance:

$def with (title,name)
 <!DOCTYPE html>
 <html>
 <head>
          <title>$title</title>
                    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 
                              </head>
<h3>
                              Hello $name
                              </h3>

                                $if len(name) > 6:
                                    What a long name
                                $else:
                                    What a short name

</html>

Here we check if the length of name is longer than 6, if it is we display "What a long name", else we display "What a short name". For more info on this check the webpy docs.

Creating forms

Creating forms is farely easy too, look at the code example below. Change urls to:

urls = ('/','root',"/anotherpage","anotherpage","/template","templatepage","/formpage","formpage",'/(.+)', 'capture')

Add another class:

class formpage:
        def __init__(self):
                self.simpleForm = web.form.Form(web.form.Textbox('input'),web.form.Button('submit'))
                self.render = web.template.render('templates/')
        def GET(self):
                return  self.render.formtemplate(  self.simpleForm )
        def POST(self):
                inputFromForm = web.input()
                return inputFromForm.input

Create a template in the templates folder called "formtemplate": [html] $def with (form)

My Form

$:form.render()
[/html]
  • So first of all the constructor creates a form object and adds a textbox and a submit button
  • GET: we return this form in our template
  • POST: we get the input we got from the POST method and just print it on the screen. In this case it is the submitted string from our form
  • We generate a template, mind $: instead of the regular $. We use $: to indicate we want HTML instead of escaped characters.
You can find more examples on the webpy site.

Static content

You might want to serve CSS files or javascript (JQuery module). To do this we can make a folder called "static" in the root folder of our application. By default the webpy webserver will not see "localhost:8080/static/" (mind the trailing "/") as a link, but it will point to your static folder. Lets change our form template: [html] $def with (form)

My Form

$:form.render()
[/html] Create a folder called "static" in the same folder our hello.py resides. In this folder create a file called style.css and add these contents: [css] h1{ color: #444; font-family:arial; } label{ color:#B00000 ; } [/css] I like to write HTML5 and I was drilled to always validate my HTML code with the W3C validator. When I generated a form I noticed they put it in a table. Which made me feel...dirty. So I fixed this by subclassing the form class, you can use this example to make your own subclasses:

import web
from web import utils,net
class CustomForm(web.form.Form):
            def render(self):
                out='
<div id="form"> '
                for i in self.inputs:
                    html = utils.safeunicode(i.pre) + i.render() + self.rendernote(i.note) + utils.safeunicode(i.post)
                    out +=  '
<div id="%s_div"> %s %s</div>'% (i.id, net.websafe(i.description), html)
                out+= '</div>'
                return out

Instead of putting everything in a table, it adds every element in a div called 'iddiv'. So if an element is called name, it will be surrounded like this: [html] <div id="namediv"> //content

[/html] To use this sublcass look at this example. The class is contained in a file called "formsub.py". I changed the formpage class to:

class formpage:
        def __init__(self):
                self.simpleForm = formsub.CustomForm(web.form.Textbox('input'),web.form.Button('submit'))
                self.render = web.template.render('templates/')
        def GET(self):
                return  self.render.formtemplate(  self.simpleForm )
        def POST(self):
                inputFromForm = web.input()
                return inputFromForm.input

Now on your http://localhost:8080/formpage

Deploying a Webpy application on Apache2

To deploy a webpy application I use Apache2 and fast-cgi server. According to the webpy docs, it's one of the best ways to run webpy.

aptitude install libapache2-mod-fastcgi
a2enmod fastcgi

In your virtualhost write:

 Alias /hasher/hello.py/static /var/www/myapp/static
        <Directory "/var/www/myapp">
                Options -Indexes ExecCGI
                AddHandler cgi-script .py
                Order allow,deny
                allow from all
                AllowOverride All
        </Directory>
        <Directory "/var/www/myapp/static">
                Options +Indexes
        </Directory>

In the folder where your application resides, write a .htaccess with these contents:

<Files hello.py>
      SetHandler fastcgi-script
</Files>
<Files "*.pyc>
        Order Deny,allow
        Deny from all
</Files>

Where hello.py is your application and static your static folder.

The things I don't like about webpy

One of the things that should be improved is the documentation, there are still a lot of "TODO" entries.

Final word

Overal I really like this framework. In a future post I will talk about using sessions.