layout | title |
---|---|
default |
web.py 0.3 tutorial |
This is a work-in-progress
TODO: move the next paragraph over to install?
To create a website with web.py you need to know the Python programming language and have it installed. Installation instructions for Python can be found at http://python.org/ .If you don't know if Python is installed on your system, open a terminal and type python
. A great starting point to learn Python is the official [tutorial] (http://docs.python.org/tut/tut.html). If you are new to programming in general, [Think Python] (http://www.greenteapress.com/thinkpython/) is a wonderful book to understand key concepts in programming.
TODO: ...
This tutorial assumes that both Python and web.py are installed on your system. If this is not the case, please follow the [installation instructions] (http://webpy.org/install) before you continue.
Furthermore, basic HTML knowledge is needed to understand some examples.
Open your favorite text editor and create a new file hello.py
. In this file you will define the content and logic of your web application as well as its web addresses (URLs).
Before you are able to use the tools web.py provides, you need to import the web.py module with the following code:
import web
In web.py web pages are mapped to Python classes. Let's create the code for the first page which is here called hello
:
class hello:
def GET(self):
return "Hello, Web!"
The hello
class has a function named GET
which returns "Hello, Web!". Why GET
?
When you open a web page, your browser asks for the content of that page. This request is called the GET
method. web.py uses the same terminology. The string your GET
method returns is displayed in your browser.
Although the code for your first page is written, it cannot yet be opened in a browser. Let's proceed with mapping a web address (URL) to your class. Insert the following code after the import statement:
urls = (
'/', 'hello')
This tells web.py to map the root of your website (like http://webpy.org/) to your Python class named hello
.
Next create an instance of a web.py application. This instance will be the mediator between your classes and the web. It will handle browser requests and serve your pages. (In short: It will do everything that you really don't want to care about.) Use the following code:
app = web.application(urls, globals())
Note that web.application()
gets called with two arguments. Your URL mapping (urls
) and your global namespace which contains your hello
class (globals()
).
To finish your web.py application insert the following code at the end of your code:
if __name__ == "__main__":
app.run()
app.run()
starts the web application to serve requested pages.
hello.py
import web
urls = (
'/', 'hello')
app = web.application(urls, globals())
class hello:
def GET(self):
return 'Hello, web!'
if __name__ == "__main__":
app.run()
Save the file and run the following command to start your application:
python hello.py
The first output of your application is the address of your web site. By default this is:
http://locahost:8080/
Open this address with your web browser. That's it. Congrats! You can stop your application at any time by pressing ctrl+c
in the terminal.
Note: You can also visit your site at http://localhost:8080/
In this part you will learn how to manage multiple pages. Let's add another class to your 'Hello Web' application:
class bye:
def GET(self):
return 'Bye, web!'
As mentioned above, each page needs a unique address. Modify your list of URLs as follows:
urls = (
'/', 'hello',
'/bye', 'bye')
This will make your class bye
respond to requests at /bye/
. Now start your application and open http://localhost:8080/bye/
in your browser.
Note: Currently you need to restart your application to see any changes. Try to pass a third argument to web.application
and restart your application:
app = web.application(urls, globals(), True)
Future changes can now be seen instantly, although you might need to reload a page in your browser.
hello.py
import web
urls = (
'/', 'hello',
'/bye', 'bye')
app = web.application(urls, globals(), True)
class hello:
def GET(self):
return 'Hello, web!'
class bye:
def GET(self):
return 'Bye, web!'
if __name__ == "__main__":
app.run()
The examples shown above are simple; all it does it display the same message, every time. Suppose you want your app to greet people by name?
Change the "hello" class above so it looks like this:
class hello:
def GET(self):
i = web.input(name = 'web')
return 'Hello, ' + web.websafe(i.name) + '!'
Run the script, and then go to http://localhost:8080/?name=Luke . You should see "Hello, Luke!."
Here is what is happening:
When you add the "?name=Luke" to the end of your web request, you are passing the variable "name" to the web server, with a value of "Luke."
The line i = web.input(name = "web")
creates a Storage object (a fancy type of Python dictionary) that contains all variables passed into it. You can also do this by just calling i = web.input()
. Here, by putting name = 'web'
into the call, we tell it to use the string "web" as a default, in case the user didn't pass in a "name" variable at all.
We read the "name" value from i by just saying i.name
. It's also possible to do i['name']
; use the syntax you prefer.
Finally, we pass i.name through the web.websafe
function before returning it to the user. If your page is being served as HTML, rather than text, then this is an important security step to protect against cross-site scripting attacks. (As we will see, web.py's form templates offer built-in protection to those attacks).
Making the user type in something like "http://localhost:8080/?name=Luke" is so 1996; wouldn't it be nicer if we could just get the user to enter "http://localhost:8080/Luke"?
Try this code:
import web
urls = (
'/(.*)', 'hello')
app = web.application(urls, globals())
class hello:
def GET(self, name):
return 'Hello, ' + web.websafe(name) + '!'
if __name__ == "__main__":
app.run()
Notice that two things have changed:
- The urls has a "(.*)" thing in it
- The
GET
method now takes two parameters - There's no more
web.input
call.
As you can see, web.py is parsing the URL for you, based on a regular expression that you provide in the urls
tuple.
Until now your classes returned only simple strings. Let's add some HTML. This can be done directly from inside your hello.py
. Replace your class hello
with this code:
class hello:
def GET(self):
return """<html>
<head>
<title>Hello, web!</title>
</head>
<body>
<h1>web.py</h1>
<p>Think about the <em>ideal</em> way to write a web app. Write the code to <b>make it happen</b>.</p>
</body>
</html>"""
Note that your page now has a custom title and HTML formatted content.
hello.py
import web
urls = (
'/', 'hello',
'/bye', 'bye')
app = web.application(urls, globals(), True)
class hello:
def GET(self):
return """<html>
<head>
<title>Hello, web!</title>
</head>
<body>
<h1>web.py</h1>
<p>Think about the <em>ideal</em> way to write a web app. Write the code to <b>make it happen</b>.</p>
</body>
</html>"""
if __name__ == "__main__":
app.run()
Imagine a larger site with many pages. If all HTML for these pages is embedded into your Python code, things get messy and your code unmaintainable. Also reusing parts of your HTML code for other pages would be difficult. Therefore web.py lets you define site layout templates that can be shared between your pages.
First create a directory templates
next to your hello.py
file. Create a file hello.html
and save it in templates
. This file will contain the HTML markup that is used to render your page. Start with the following basic template:
$def with (title, name, content)
<html>
<head>
<title>$title</title>
</head>
<body>
<p>You are visiting page <b>$name</b>.</p>
<p>$content</p>
</body>
</html>
Create a second template bye.html
:
$def with (title, name, *numbers)
<html>
<head>
<title>$title</title>
</head>
<body>
<p>You are visiting page <b>$name</b>.</p>
<p>Find the answer to all questions below:
$for number in numbers:
<p>$number</p>
</body>
</html>
Besides defining a page structure, these templates will use variables. The first line of hello.html
(def with (title, name, number)
) will tell web.py that this template needs to be called with three arguments. Wherever $title
is used in the template the actual value of title
is inserted.
Arguments of bye.html
are title
, name
and an arbitrary number of numbers (*numbers
). All arguments beside title
and name
are put into the list numbers
. This list is then iterated ($for number in numbers:
) and each number (number
) is written in its own paragraph. You see that $
is not only used to access template variables but also to evaluate (safe) Python code like for
loops or if
statements.
Now insert the following line before your class definitions to create a so called template renderer. The location of your templates is passed to the renderer as an argument:
render = web.template.render('templates/')
Next modify your classes to render your pages using the two different templates:
class hello:
def GET(self):
return render.hello("Templates demo", "Hello", "A long time ago...")
class bye:
def GET(self):
return render.bye("Templates demo", "Bye", "14", "8", "25", "42", "19")
Open the pages in your browser. web.py fetches your templates and dynamically inserts the values that you passed to your templates.
hello.py
import web
urls = (
'/', 'hello',
'/bye/', 'bye')
app = web.application(urls, globals(), True)
render = web.template.render('templates/')
class hello:
def GET(self):
return render.hello("Templates demo", "Hello", "A long time ago...")
class bye:
def GET(self):
return render.bye("Templates demo", "Bye", "14", "8", "25", "42", "19")
if __name__ == "__main__":
app.run()
templates/hello.html
$def with (title, name, content)
<html>
<head>
<title>$title</title>
</head>
<body>
<p>You are visiting page <b>$name</b>.</p>
<p>$content</p>
</body>
</html>
templates/bye.html
$def with (title, name, *numbers)
<html>
<head>
<title>$title</title>
</head>
<body>
<p>You are visiting page <b>$name</b>.</p>
<p>Find the answer to all questions below:
$for number in numbers:
<p>$number</p>
</body>
</html>
The previous example defined two templates but both had duplicate code. In most cases your pages share a lot of common code like a navigation bar or a footer. Let's create a file base.html
which contains all the code your pages share with each other:
$def with (page)
<html>
<head>
<title>$page.title</title>
</head>
<body>
<p>You are visiting page <b>$page.name</b>.</p>
$:page
</body>
</html>
This base template receives only one variable page
. $page.title
is a placeholder for a variable named title
defined in a child template. $:page
is a placeholder for everything else that you put in your child template. Modify hello.html
to be a child template:
$def with (title, name, content)
$var title:$title
$var name:$name
<p>$content</p>
The previously duplicated code for the HTML body, the page title and the current page information is gone. Instead $var title:$title
tells the base template to use the local title
as $page.title
. The remaining line <p>$content</p>
will be available in the base template as $:page
.
Modify bye.html
accordingly:
$def with (title, name, *numbers)
$var title:$title
$var name:$name
<p>Find the answer to all questions below:</p>
$for number in numbers:
<p>$number</p>
The last step is to tell web.py to use base.html
as the base template. Use the following code (you might need to replace your previous code):
render = web.template.render('templates/', base='base')
Both hello.html
and bye.html
will now use base.html
.
hello.py
import web
urls = (
'/', 'hello',
'/bye/', 'bye')
app = web.application(urls, globals(), autoreload=True)
render = web.template.render('templates/', base='base')
class hello:
def GET(self):
return render.hello("Templates demo", "Hello", "A long time ago...")
class bye:
def GET(self):
return render.bye("Templates demo", "Bye", "14", "8", "25", "42", "19")
if __name__ == "__main__":
app.run()
base.html
$def with (page)
<html>
<head>
<title>$page.title</title>
</head>
<body>
<p>You are visiting page <b>$page.name</b>.</p>
$:page
</body>
</html>
hello.html
$def with (title, name, content)
$var title:$title
$var name:$name
<p>$content</p>
bye.html
$def with (title, name, *numbers)
$var title:$title
$var name:$name
<p>Find the answer to all questions below:</p>
$for number in numbers:
<p>$number</p>
Now that your application serves HTML formatted content, you probably want to include static files like images or css style files. To achieve this create a directeory called static
next to your hello.py
file. Put a picture file (here called logo.png
) in your static
directory. Then include the file on your page:
class hello:
def GET(self):
return """<img src="./static/logo.png">"""
hello.py
import web
urls = (
'/', 'hello')
app = web.application(urls, globals(), web.reloader)
class hello:
def GET(self):
return """<img src="./static/logo.png">"""
if __name__ == "__main__":
app.run()
User input (HTML forms and the POST
method) [cookbook] (http://webpy.org/form)
Until now GET
functions were introduced to serve pages but there was no way a user could send data back to your application. A function called POST
will allow this. To use POST
you need to create form fields on your page where a user can input his data. Let's make hello.py
return a page that contains HTML forms using web.py form
module. To reduce typing add the following import statement:
from web import form
Now define a form before your classes in hello.py
. This example only uses a single input field. Visit the [cookbook] (http://webpy.org/form) for more advanced types. The following code gives you a text box with validation of the input:
number_form = form.Form(
form.Textbox('number',
form.notnull,
form.regexp('^-?\d+$', 'Not a number.'),
form.Validator('Not greater than 10.', lambda x: int(x)>10),
description='Enter a number greater than 10:'
))
form.Textbox()
creates an HTML text box. The first parameter specifies its name: 'number'
. Most often you will want to validate the input of a user instantly and allow him to correct errors. form.notnull
makes it a required field that cannot be left empty. form.regexp()
matches the input with the given regular expression. Here it is checked if the input is a number. form.Validator()
additionally checks if the input is a number greater ten. And finally, description
is the text that is printed in front of the text box.
Now make your template hello.html
accept and display a form:
$def with (form)
<form name="test" method="POST">
$if not form.valid: <p>Sorry, your input was invalid.</p>
$:form.render()
<input type="submit" value="Check" />
</form>
Notice that the template will print an error message if the form input is invalid.
And finally your hello
class needs the following GET
and POST
methods:
class hello:
def GET(self):
my_form = number_form()
return render.hello(my_form)
def POST(self):
my_form = number_form()
if not my_form.validates():
return render.hello(my_form)
else:
number = my_form['number'].value
if int(number) % 2:
return "Your number %s is odd." % number
else:
return "Your number %s is even." % number
When you visit hello
in your browser, the GET
method creates an instance of your form and returns the rendered page. Enter a number greater 10 and press the Check
button. Now the POST
method is invoked to process your input. Because the GET
and POST
methods cannot access the same form instance a new one is created. form.validates()
checks the input you entered. But how does it know what you have entered? By default the validates()
method fetches your input from web.input()
where it is stored as soon as you press the Check
button. In case your input is invalid, the form is returned again. Else my_form['number'].value
is retrieved which is the number you entered and your application will tell you if you entered an even or odd number.
Note: You cannot access form values before having validated the form!
Now go back and try some invalid input. First leave the text field blank and press Check
. You will be informed that you left a required field blank. Enter some text and you will get a "Not a number" message. This is due to the regular expression check. And finally try some number that is not greater than ten. The form input will not be validated and you are advised to enter a number greater ten.
hello.py
import web
from web import form
urls = (
'/', 'hello')
app = web.application(urls, globals(), web.reloader)
render = web.template.render('templates/')
number_form = form.Form(
form.Textbox('number',
form.notnull,
form.regexp('^-?\d+$', 'Not a number.'),
form.Validator('Not greater 10.', lambda x: int(x)>10),
description='Enter a number greater 10:'
))
class hello:
def GET(self):
my_form = number_form()
return render.hello(my_form)
def POST(self):
my_form = number_form()
if not my_form.validates():
return render.hello(my_form)
else:
number = my_form['number'].value
if int(number) % 2:
return "Your number %s is odd." % number
else:
return "Your number %s is even." % number
if __name__ == "__main__":
app.run()
hello.html
$def with (form)
<form name="test" method="POST">
$if not form.valid: <p>Sorry, your input was invalid.</p>
$:form.render()
<input type="submit" value="Check" />
</form>
Sessions [cookbook] (http://webpy.org/cookbook/sessions)
ATTENTION: Sessions cannot be used with web.reloader
at the time of this writing! It is a known bug.
Many sites need to distinguish its visitors. Imagine you want to show the user the number of pages he visited on your page. Each visitor has a unique number. To allow separate tracking web.py uses so called sessions. Each visitor gets his very own session object in which his unique number is saved. First create a session object in hello.py
. Put this line after your app
is initialized:
session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'count': 0})
This creates a session object. The first parameter is simply the application the session object is used for. web.session.DiskStore('sessions')
tells web.py to store sessions on disk (database storage is possible as well, see this [cookbook] (http://webpy.org/cookbook/sessions) entry). The third optional parameter initializes the session data dictionary for each user. Here each session object starts with zero visited pages (count
). web.py creates a directory sessions
to store session data on disk. Modify your classes in hello.py
like this:
class hello:
def GET(self):
session.count += 1
return "You visited " + str(session.count) + " pages."
class bye:
def GET(self):
session.kill()
return ("Bye, web!")
Each time you visit hello
, the number of pages you visited is incremented (session.count += 1
). If you visit bye
the session is killed (session.kill()
). The next time you visit hello
, a new session will be created and the counter will be zero again.
hello.py
import web
web.config.debug=False
urls = (
'/', 'hello',
'/bye/', 'bye')
app = web.application(urls, globals())
session = web.session.Session(app, web.session.DiskStore('sessions'),
initializer={'count': 0})
class hello:
def GET(self):
session.count += 1
return "You visited " + str(session.count) + " pages."
class bye:
def GET(self):
session.kill()
return ("Bye, web!")
if __name__ == "__main__":
app.run()
This is an example of user authentication using a session cookie, the most common method. Most often user authentication is done by providing functions to login and logout a user. Additionally, users often are able to register or delete their account.
hello.py
import web
from web import form
import random
from hashlib import sha1
# A simple user object that doesn't store passwords in plain text
# see http://en.wikipedia.org/wiki/Salt_(cryptography)
class PasswordHash(object):
def __init__(self, password_):
self.salt = "".join(chr(random.randint(33,127)) for x in xrange(64))
self.saltedpw = sha1(password_ + self.salt).hexdigest()
def check_password(self, password_):
"""checks if the password is correct"""
return self.saltedpw == sha1(password_ + self.salt).hexdigest()
# Note: a secure application would never store passwords in plaintext in the source code
users = {
'Kermit' : PasswordHash('frog'),
'ET' : PasswordHash('eetee'),
'falken' : PasswordHash('joshua') }
urls = ('/', 'hello',
'/logout/', 'logout',
'/register/', 'register')
app = web.application(urls, globals())
render = web.template.render('templates/')
if web.config.get('_session') is None:
session = web.session.Session(app, web.session.DiskStore('sessions'),
initializer={'user': 'anonymous'})
web.config._session = session
else:
session = web.config._session
signin_form = form.Form(form.Textbox('username',
form.Validator('Unknown username.',
lambda x: x in users.keys()),
description='Username:'),
form.Password('password',
description='Password:'),
validators = [form.Validator("Username and password didn't match.",
lambda x: users[x.username].check_password(x.password)) ])
signup_form = form.Form(form.Textbox('username',
form.Validator('Username already exists.',
lambda x: x not in users.keys()),
description='Username:'),
form.Password('password',
description='Password:'),
form.Password('password_again',
description='Repeat your password:'),
validators = [form.Validator("Passwords didn't match.",
lambda i: i.password == i.password_again)])
class hello:
def GET(self):
my_signin = signin_form()
return render.hello(session.user, my_signin)
def POST(self):
my_signin = signin_form()
if not my_signin.validates():
return render.hello(session.user, my_signin)
else:
session.user = my_signin['username'].value
return render.hello(session.user, my_signin)
class logout:
def GET(self):
session.kill()
raise web.seeother('/')
class register:
def GET(self):
my_signup = signup_form()
return render.signup(my_signup)
def POST(self):
my_signup = signup_form()
if not my_signup.validates():
return render.signup(my_signup)
else:
username = my_signup['username'].value
password = my_signup['password'].value
users[username] = PasswordHash(password)
raise web.seeother('/')
if __name__ == "__main__":
app.run()
hello.html
$def with (user, form)
$if user == 'anonymous':
<p>You are not logged in.</p>
<p>
<form name="test" method="POST">
$:form.render()
<input type="submit" name="button" value="Login" />
</form>
</p>
<p><a href="./register/">Register</a></p>
$else:
<p>You are logged in as: $user</p>
<p><a href="./logout/">Logout</a></p>
signup.html
$def with (form)
<form name="test" method="POST">
$:form.render()
<input type="submit" value="Register" />
</form>
Advantages / disadvantages of different solutions: App Engine, servers...
TODO: ...