Python Lesson 03 - Standard Library and Friends

In lesson one, we learned about many of Python's basic constructs. In lesson two, we learned about building classes, and using code in the standard library. In this lesson, we will look at a few more standard library modules. We'll also get Python to generate us a web page using the CGI interface.

Contents

A Review of Lesson 02

Here is the customer class from last week. Make sure you can explain all its parts. Make sure you can write a class on your own that can use all of the same features as these classes do:

#!/usr/bin/python

class customer(object):
    company="Bluehost"
    def __init__(self, name, domain):
        self.name = name
        self.domain = domain
    def domain_expire(self):
        message = "Hello, %s. Your domain name, %s, has expired." % \
                  (self.name, self.domain)
        return message
    def __str__(self):
        return """Name: %s
Domain: %s
Company: %s
        """ % (self.name, self.domain, self.company)
class hmcustomer(customer):
    company="Hostmonster"

More Standard Library Modules

optparse

This module makes it extremely easy to add and parse command line options to our scripts.

Let's take a look at the example from the official docs:

#!/usr/bin/python
from optparse import OptionParser

parser = OptionParser()
parser.add_option("-f", "--file", dest="filename",
                  help="write report to FILE", metavar="FILE")
parser.add_option("-q", "--quiet",
                  action="store_false", dest="verbose", default=True,
                  help="don't print status messages to stdout")

(options, args) = parser.parse_args()

#these two lines were added to show what's going on under the hood.
print options
print args

I put my code in a file called opt.py, and this is how I can use it:

jefferya@tam:~$ python opt.py
{'verbose': True, 'filename': None}
[]
jefferya@tam:~$ python opt.py -f nothing.txt
{'verbose': True, 'filename': 'nothing.txt'}
[]
jefferya@tam:~$ python opt.py -q -f nothing.txt
{'verbose': False, 'filename': 'nothing.txt'}
[]
jefferya@tam:~$ python opt.py -q
{'verbose': False, 'filename': None}
[]
jefferya@tam:~$ python opt.py -h
Usage: opt.py [options]

Options:
  -h, --help            show this help message and exit
  -f FILE, --file=FILE  write report to FILE
  -q, --quiet           don't print status messages to stdout

smtplib (revisited)

Now that we have optparse in our toolbelt, let's spice up the smtplib example from last time:

#!/usr/bin/python

import smtplib

def send(from_addr, to_addr, subject="Test", \
        mail_body="This is a test message", mail_server="localhost", \
        mail_server_port=25, authuser="", authpass="", verbosity=0):
    from_hdr = 'From: %s' % from_addr
    to_hdr = 'To: %s' % to_addr
    subject_hdr = "Subject: %s" % subject
    s = smtplib.SMTP(mail_server, mail_server_port)
    s.set_debuglevel(verbosity)
    ehlo_res = s.ehlo()
    s.starttls()

    if "AUTH PLAIN LOGIN" in ehlo_res[1] and authuser:
        s.login(authuser, authpass)
    else: print "not using authentication"

    email_message="%s\n%s\n%s\n\n%s" % (from_hdr, to_hdr, subject_hdr, mail_body)
    s.sendmail(from_addr, to_addr, email_message)
    s.quit()

if __name__=="__main__":
    import optparse
    from optparse import OptionParser

    parser = OptionParser(usage="usage: %prog -f ADDRESS -t ADDRESS [options]")
    parser.add_option("-f", "--from", dest="from_addr", \
            help="Senders address", metavar="ADDRESS")
    parser.add_option("-t", "--to", dest="to_addr", \
            help="Destination address", metavar="ADDRESS")
    parser.add_option("-u", "--user", dest="authuser", \
            help="SMTP authentication user", metavar="USERNAME", default=None)
    parser.add_option("--pass", "--password", dest="authpass", \
            help="SMTP authentication password", metavar="PASSWORD", default=None)
    parser.add_option("-s", "--server", dest="mail_server", \
            help="Mail server hostname", metavar="HOST", default="localhost")
    parser.add_option("-p", "--port", dest="mail_server_port", \
            help="Mail server port", metavar="PORTNUM", type="int", default=25)
    parser.add_option("-v", "--verbosity", dest="verbosity", \
            help="smtplib debug value", metavar="VAL", type="int", default=1)

    (options, args) = parser.parse_args()
    if not (options.from_addr and options.to_addr):
        parser.error("You must supply both a sending and destination address.")
    send(**eval(str(options)))

That's a lot of code to look at all at once, but just take a look one line at a time. Effectively, what this file, smtplibopt.py, is, is a hybrid. I can run it from the command line, and get a regular and complete set of features. I can also import it from the Python interpretor, or from other code, and use the send function that way too.

There is a cool piece of trickery going on here. The multiple assignment line: (options, args) = parser.parse_args() assigns an optparse.Values instance to options. This doesn't behave like a standard dictionary, but its __str__ method produces output identical to a dictionary definition. eval takes a string and treats it like actual Python code. In this case, it returns a dictionary, which we can then pass directly to send.

Let's try using this concoction from the command line:

jefferya@tam:~$ python smtplibopt.py -h
Usage: smtplibopt.py -f ADDRESS -t ADDRESS [options]

Options:
  -h, --help            show this help message and exit
  -f ADDRESS, --from=ADDRESS
                        Sender address
  -t ADDRESS, --to=ADDRESS
                        Destination address
  -u USERNAME, --user=USERNAME
                        SMTP authentication user
  --pass=PASSWORD, --password=PASSWORD
                        SMTP authentication password
  -s HOST, --server=HOST
                        Mail server hostname
  -p PORTNUM, --port=PORTNUM
                        Mail server port
  -v VAL, --verbosity=VAL
                        smtplib debug value
jefferya@tam:~$ python smtplibopt.py
Usage: smtplibopt.py -f ADDRESS -t ADDRESS [options]

smtplibopt.py: error: You must supply both a sending and destination
address.
jefferya@tam:~$ python smtplibopt.py -f jefferya@programmerq.net -t
pinguino@box552.bluehost.com -u pinguino --pass my_password -s
box552.bluehost.com -v 0

webbrowser

This is basically a remote control for your web browser, whatever it may be.

Let's jump right in:

>>> import webbrowser
>>> dir(webbrowser)
['BackgroundBrowser', 'BaseBrowser', 'Elinks', 'Error', 'Galeon',
'GenericBrowser', 'Grail', 'Konqueror', 'Mozilla', 'Netscape', 'Opera',
'UnixBrowser', '__all__', '__builtins__', '__doc__', '__file__',
'__name__', '__package__', '_browsers', '_iscommand', '_isexecutable',
'_synthesize', '_tryorder', 'get', 'main', 'open', 'open_new',
'open_new_tab', 'os', 'register', 'register_X_browsers', 'shlex', 'stat',
'subprocess', 'sys', 'time']

Let's make our customer.py helpful, by adding a cpm and cpanel function that will open up the respective tool for the customer's domain:

def cpm(self):
    import webbrowser
    webbrowser.open_new_tab("https://www.bluehost.com/" + \
    "cgi/admin/user/cpanel/%s" % self.domain)
def cpanel(self):
    import webbrowser
    webbrowser.open_new_tab("https://www.bluehost.com/" + \
    "cgi/admin/user/cpanel_login/%s" % self.domain)

CGI Script - by hand

This section assumes you are already familiar with CGI scripts. They can be written in any language. For info on the CGI spec, please see http://hoohoo.ncsa.illinois.edu/cgi/.

The CGI interface is a standard way of allowing a program to generate web pages, process form data, get information about the web server, etc. We'll implement a very simple Python CGI script that can output a simple page, issue a redirect, and even take POST data.

Simple Page

A quick test script that employs the CGI interface has proven useful to me many times:

#!/usr/bin/python
print "Content-type: text/plain"
print
print "Hello World"

import datetime
print "The time now is:", datetime.datetime.now()

Redirect

Because CGI scripts output any headers, you can easily redirect a browser:

#!/usr/bin/python
print "Status: 303 See Other"
print "Location: http://www.google.com/"
print "Content-type: text/plain"
print
print "The requested page has moved. Please google your question instead."

POST

POSTing to a CGI script can be a bit tricky. Post data is in one big long string that gets passed to the CGI script via standard input. Let's just regurgitate that data back to the user:

#!/usr/bin/python
print "Content-type: text/plain"
print
print "Here is your data:"
import sys
for i in sys.stdin.readlines():
    print i

So, that has a gotcha in it. This script terminates after saying "Here is your data:" no matter if you have POSTed anything to it or not. POST data is passed in on stdin, but it includes no newlines, and no EOF character. readlines only returns something if it can tell where either a newline, or an EOF is. The CGI spec was designed with a way to tell the cgi script how much data there is. If the script reads too little data, then some would be lost. If it tries to read too much, it would be stuck waiting for more input to come in on stdin.

The CGI script has many environmental variables that are set, so the script can get information about how and where it is being run. One such variable is called CONTENT_LENGTH. We can read exactly the right number of bytes if we look up this variable:

#!/usr/bin/python
print "Content-type: text/plain"
print
print "Here is your data:"
import sys, os
print sys.stdin.read(int(os.environ['CONTENT_LENGTH']))

You can take any HTML form, and use it to POST to this script, and see what the webserver receives. This version works.

CGI Script - cgi module

We've implemented a couple things using very simple CGI. We have access to all sorts of things that we can do by hand, but sometimes it's just silly to do things by hand. We even ran into a snag when trying to process POST data. When a project gets larger than one or two screens worth of code, abstracting this mundane stuff makes things much more manageable and readable. The cgi module is designed to help you do just this.

Let's start off with some nice form processing:

#!/usr/bin/python

print "Content-type: text/html"
print

import cgi
form = cgi.FieldStorage()
if not (form.has_key("name") and form.has_key("addr")):
    print "<H1>Error</H1>"
    print "Please fill in the name and addr fields."
    return
cgi.print_form(form)

All this form does is create a FieldStorage instance. This class is smart enough to look and see that POST or GET data is present, and parse it into something a bit more useful. It checks to make sure that there is a key called "name" and a key called "addr". If either is not present, it complains.


There is another module called cgitb. The cgi module docs urge the reader to implement the cgitb module. cgitb takes any Exceptions that Python may encounter during the execution of the script, and formats it into HTML. This makes debugging less painful. You can also use cgitb to do something else with the Traceback, like save it to a log file or e-mail it. It's generally a bad idea to expose Traceback information to a hostile network like the Internet.

Let's modify our previous example a bit:

#!/usr/bin/python

print "Content-type: text/html"
print

import cgi, cgitb
cgitb.enable()
form = cgi.FieldStorage()
if not form.has_key("port"):
    print "<H1>Error</H1>"
    print "Please fill in the port field."
    return
port = int(form['port'].value)
cgi.print_form(form)

I've changed this to look for a "port" key instead of "name" and "addr" keys. It then tries to store an integer version of the "port" value to a Python variable called port. As long as the user submits a valid integer as the value for the "port" field, then the code will execute just fine. If they submit something silly, like letters, or a decimal, the casting to an int will fail and raise an exception. Without cgitb this would have simply failed with a 500 Internal Server error, and absolutely no helpful output.

Try posting to this script with "port" set to "twenty" and again set to "20" to see how it behaves.

cgitb makes it very nice and very easy to diagnose things during application development. Most new web applications don't implement just plain old cgi interfaces anymore, but almost all other interfaces are based on cgi to some degree.

Homework

Your homework is to write a cgi script in Python that takes form input, and displays it back to the user in nicely formatted HTML.

If you feel really ambitious, make a form mailer script using cgi, cgitb, and smtplib.

Contact

If you have any questions, please post them to the mailing list on the Google Group.

I'm also happy to answer questions in person. This document will also be updated to reflect questions and concerns that come up both in person and on the mailing list.

Note

License and Legal

This document is Copyright (c) Jeff Anderson. It is not freely distributable. It is intended to be distributed amongst employees of Bluehost for the purpose of learning the Python language. Any copying or distribution beyond this is not permitted. These terms may be modified at any time by the author.

This notice applies only to the content. The HTML code generated by docutils is in the public domain. The syntax highlighting Javascript is google-code-prettify.