Explore 8

Beautiful Code
Ka-Ping Yee, March 12, 2003

1. Unit Testing

Now that you've had a little experience writing tests, we'll introduce you to a test harness called unittest that make tests more convenient.

To use unittest, you write a separate Python file that defines a series of classes, where each class is derived from the unittest.TestCase class, and has a test() method that uses assert to verify something. Then you call unittest.main(). This will automatically run all of your tests and produce a report on any failures.

A simple example should help to make this more clear:

from unittest import TestCase, main

class TestAddition(TestCase):
    def test(self):
        assert 1 + 1 == 2, "addition produced an incorrect result"

class TestSubtraction(TestCase):
    def test(self):
        assert 5 - 4 == 2, "subtraction produced an incorrect result"

class TestMultiplication(TestCase):
    def test(self):
        assert 2 * 2 == 4, "multiplication produced an incorrect result"

class TestDivision(TestCase):
    def test(self):
        assert 3 / 0 == 1, "division produced an incorrect result"

main()

Here's what you'll see if you run the above script:

.E.F
======================================================================
ERROR: test (__main__.TestDivision)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "arith.py", line 17, in test
    assert 3 / 0 == 1, "division produced an incorrect result"
ZeroDivisionError: integer division or modulo by zero

======================================================================
FAIL: test (__main__.TestSubtraction)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "arith.py", line 9, in test
    assert 5 - 4 == 2, "subtraction produced an incorrect result"
AssertionError: subtraction produced an incorrect result

----------------------------------------------------------------------
Ran 4 tests in 0.003s

FAILED (failures=1, errors=1)

The first line of output is a summary of the tests that succeeded or failed. It shows a dot for success, then an E for error, then a dot for success, then an F for failure, corresponding to the four tests in alphabetical order (TestAddition, then TestDivision, then TestMultiplication, then TestSubtraction). Then there are more detailed reports on any tests that didn't succeed.

Notice the distinction between a failure (an assertion failed, which generally means that some expression produced an unexpected result) and an error (an error occurred while trying to perform the test).

A couple of notes about this:

  1. You can put as many assertions as you like in a single test method. The ability to group the tests under classes is provided just to help you organize your tests.
  2. If you want some preparations to take place before the actual test, you can put the preparatory work into a method called setUp(). Then, you can derive test classes from other test classes, so they can share the same setUp() method without you having to write the setup code twice.
  3. There are many more ways to use unittest if you want to do fancier things. For now, we're sticking to the straightforward way.

Q1. Find your partner for the last homework assignment. Pick just one or two tests from your test suite, and convert them over to use unittest. The result should be a script that imports both unittest and the appropriate date module, then defines a class or two derived from unittest.TestCase, and finally calls unittest.main(). Call me over to show me your work.

Do not work on Q1 past 5:10 pm. If it's after 5:10 pm, move on.

I'll be asking you to use the unittest style when you submit the tests for your projects.

2. XML

Here we'll just do a brief introduction to the XML facilities in Python. Again, you can go much deeper into this if you want, but this might help you get started.

Python provides a package called xml. A package is a collection of modules. We'll talk more about packages later, but the main point is just that you can use dots to form a path to a module within a package. The xml package contains a small implementation of the "XML Document Object Model" (the "DOM") in the module xml.dom.minidom.

This module can parse XML into a corresponding tree-like data structure in memory. Here's an example:

>>> doc = '<spam>ick<eggs type="scrambled">yum</eggs></spam>'
>>> top = xml.dom.minidom.parseString(doc)
>>> top.childNodes
[<DOM Element: spam at 136293436>]
>>> n = top.childNodes[0]
>>> n.childNodes
[<DOM Text node "ick">, <DOM Element: eggs at 136658772>]
>>> n.childNodes[0]
<DOM Text node "ick">
>>> n.childNodes[0].data 
u'ick'
>>> n.childNodes[1]
<DOM Element: eggs at 136658772>
>>> n.childNodes[1].childNodes
[<DOM Text node "yum">]
>>> n.childNodes[1].childNodes[0].data
u'yum'
>>> n.getElementsByTagName('spam')
[]
>>> top.getElementsByTagName('spam')
[<DOM Element: spam at 136293436>]
>>> top.getElementsByTagName('eggs')
[<DOM Element: eggs at 136658772>]
>>> 

As you can see, each node in the tree has an attribute called childNodes that lists its children. You can walk your way down to the nodes a level at a time, by asking successive nodes for their children, or you can collect all the nodes for a particular tag using the getElementsByTagName() method, which searches an entire subtree.

Text can be extracted from text nodes using the data attribute.

For nodes representing tags, the tag name is available in the tagName attribute. The tag attributes can be obtained by looking at the node's attributes attribute, which retrieves something similar to a dictionary:

>>> enode = _[0] 
>>> enode.attributes
<xml.dom.minidom.NamedNodeMap instance at 0x8252e74>
>>> enode.attributes.keys()
[u'type']
>>> enode.attributes['type']
<xml.dom.minidom.Attr instance at 0x825421c>
>>> enode.attributes['type'].value
u'scrambled'
>>> 

The little u that appears in front of the strings indicates that the strings use Unicode, a standard for encoding strings containing characters from all the world's languages. XML is officially specified to use Unicode for all of its text.

The parseString() routine is only good if you have an entire XML document in a string. Alternatively, you can parse an open stream (such as the one produced by urllib.urlopen) using xml.dom.minidom.parse(). That's probably what you will want to use for the following exercise.

Q2. The URL http://slashdot.org/slashdot.rss provides an XML document describing the current headlines at Slashdot. Have a look at this document to see what tags are used to mark the headlines. Using urllib and xml.dom.minidom together, parse the Slashdot XML into a document object. Then use getElementsByTagName to extract the headline titles as a list of strings. (I'm not asking you to write a program; just play around in the interpreter until you've extracted the titles, and send me a transcript.)

Do not work on Q2 past 5:30 pm. If it's after 5:30 pm, move on.

3. Tkinter

This is just a brief look at Tkinter, the GUI toolkit that comes bundled with Python. We'll look at this more in depth next week, but i wanted to give you a chance to play with it first.

The machines in EECS do not have Tkinter installed. So accounts have been created for you on another machine that does have Python with Tkinter. To get to that account, just type

ssh crit.org

You may be asked whether you want to accept a new host key. Say "yes" and you should be automatically logged in to the other account.

Try the following.

from Tkinter import *
b = Button(text='Press me!')
b.pack()

This should get you a nice little button. You can configure many properties of the button using the config() method.

b.config(text='Wuggeda wuggeda wuggeda doo-wop wah.')
b.config(foreground='yellow')
b.config(background='blue')

Calling b.config() by itself produces a dictionary of all the configuration options. It's big. Here's a list of the keys:

>>> b.config().keys()
['bd', 'foreground', 'bg', 'highlightthickness', 'text', 'image', 'height',
 'borderwidth', 'background', 'fg', 'pady', 'padx', 'bitmap',
 'activeforeground', 'activebackground', 'highlightbackground',
 'disabledforeground', 'wraplength', 'font', 'width', 'default',
 'underline', 'cursor', 'textvariable', 'state', 'highlightcolor',
 'command', 'relief', 'anchor', 'takefocus', 'justify']
>>>

All of these are acceptable both as keyword arguments to the config() method (e.g. b.config(borderwidth=10)), or as keyword arguments to the Button() constructor.

Some of them require a little explanation:

Experiment with the various options and see what you can do.

This can make some pretty buttons, but pressing the button doesn't do anything yet. That's what the command option is for: you can set the command to any Python function, and the function will get called when the button is pressed. Try it:

def pressed():
    print 'Eek!'

b.config(command=pressed)

You might have noticed that the button didn't appear until you called its pack() method. That method invokes the packer, which manages the layout of various items in the window. The packer arranges things by placing them along one side of the available space. You can choose a side using the side argument:

b2 = Button(text='hello')
b2.pack(side='left')
b3 = Button(text='there')
b3.pack(side='right')

By default, everything is packed along the top side, which means that items will appear in a column from top to bottom.

To achieve more interesting layouts, you can pack widgets inside a Frame and then pack the Frame inside the window.

f = Frame()
b4 = Button(f, text='spam')
b4.pack(side='bottom')
b5 = Button(f, text='eggs')
b5.pack(side='top')
f.pack(side='right')

Frames have no border by default, but you can make the edge of a frame visible if you set its borderwidth to a positive number.

f.config(borderwidth=2)
f.config(relief='ridge')

Play around with this for a bit to see how the layout manager works.

Here's the source to the tic-tac-toe program. Do you see how it arranges the squares into a 3-by-3 grid? Everything is arranged in the __init__ constructor of TicBoard.


If you have any questions about these exercises or the assignment, feel free to send me e-mail at bc@zesty.ca.