Phase objects: simple decent error reporting for Python programs

September 28, 2007

For a lot of the sort of utility programs I write in Python, the only thing the program can really do when it runs into an operating system level error is report whatever the problem is and exit. So programs start out as one big try:/except: block:

try:
    fp1 = open(fnameA, "r")
    ....
    fp2 = open(fnameB, "r")
    ....
except EnvironmentError, e:
    die("OS error: %s" % str(e))

The problem with this is that the error reported, while accurate, is basically without context; you don't know what the program was doing when it reported 'permission denied' or whatever. So simple programs mutate slightly to keep track of what they are doing in a variable:

try:
    phase = "opening %s" % fnameA
    fp1 = open(fnameA, "r")
    phase = "parsing %s" % fnameA
    ....
except EnvironmentError, e:
    die("%s: error: %s" % (phase, str(e)))

(Well, my simple programs do, since I have no desire to wrap each separate operation that might fail in its own try:/except: block. That's too much work.)

The problem with this approach is that it doesn't work very well when your program grows subroutines, because updating a global variable to track the phase is a pain. The solution I have adopted is to create a special class to track what phase of execution the program is in; with my typical (lack of) imagination, I call this class Phase, and it looks like this:

class Phase(object):
    def __init__(self):
        self.phase = ["starting",]
    def __call__(self, ph):
        self.phase = [ph, ]
    def __str__(self):
        return ": ".join(self.phase)
    def push(self, ph):
        self.phase.append(ph)
    def pop(self):
        self.phase.pop()

It is used as phase("reading file whatever"). It supports push() and pop() operations so that you can have nested situations (such as the ability of configuration files to include other configuration files) that report the full chain of events of how you wound up doing whatever it was that failed.

My programs just make a single global Phase instance and then use it directly as a global variable, but I suppose a more elegant and reusable method would be to create the instance at the top level and pass it down through the call chain.

Written on 28 September 2007.
« Fixing Ubuntu's ethN device names when you swap hardware
The first rule of free email-based services »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Sep 28 22:58:54 2007
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.