We use the data to do two things. First, we’ll write a program that’s run periodically (say, at 2 a.m., every night[69]) and looks through the saved data, finds out which saved pickled files correspond to complaints, and prints out a customized letter to the complainant. Sounds sophisticated, but you’ll be surprised at how simple it is using the right tools. Joe’s web site is on a Windows machine, so we’ll assume that for this program, but other platforms work in similar ways.
Before we talk about how to write this program, a word about the technology it uses, namely Microsoft’s Common Object Model (COM). COM is a standard for interaction between programs (an Object Request Broker service, to be technical), which allows any COM-compliant program to talk to, access the data in, and execute commands in other COM-compliant programs. Grossly, the program doing the calling is called a COM client, and the program doing the executing is called a COM server. Now, as one might suspect given the origin of COM, all major Microsoft products are COM-aware, and most can act as servers. Microsoft Word Version 8 is one of those, and the one we’ll use here. Indeed, Microsoft Word is just fine for writing letters, which is what we’re doing. Luckily for us, Python can be made COM-aware as well, at least on Windows 95, Windows 98, and Windows NT. Mark Hammond and Greg Stein have made available a set of extensions to Python for Windows called win32com that allow Python programs to do almost everything you can do with COM from any other language. You can write COM clients, servers, ActiveX scripting hosts, debuggers, and more, all in Python. We only need to do the first of these, which is also the simplest. Basically, our form letter program needs to do the following things:
Open all of the pickled files in the appropriate directory and unpickle them.
For each unpickled instance file, test if the feedback is a complaint. If it is, find out the name and address of the person who filled out the form and go on to Step 3. If not, skip it.
Open a Word document containing a template of the letter we want to send, and fill in the appropriate pieces with the customized information.
Print the document and close it.
It’s almost as simple in Python with win32com . Here’s a little program called formletter.py :
from win32com.client import constants, Dispatch WORD = 'Word.Application.8' False, True = 0, -1 import string class Word: def __init__(self): self.app = Dispatch(WORD) def open(self, doc): self.app.Documents.Open(FileName=doc) def replace(self, source, target): self.app.Selection.HomeKey(Unit=constants.wdLine) find = self.app.Selection.Find find.Text = "%"+source+"%" self.app.Selection.Find.Execute() self.app.Selection.TypeText(Text=target) def printdoc(self): self.app.Application.PrintOut() def close(self): self.app.ActiveDocument.Close(SaveChanges=False) def print_formletter(data): word.open(r"h:DavidBook ofutemplate.doc") word.replace("name", data.name) word.replace("address", data.address) word.replace("firstname", string.split(data.name)[0]) word.printdoc() word.close() if __name__ == '__main__': import os, pickle from feedback import DIRECTORY, FormData, FeedbackData word = Word() for filename in os.listdir(DIRECTORY): data = pickle.load(open(os.path.join(DIRECTORY, filename))) if data.type == 'complaint': print "Printing letter for %(name)s." % vars(data) print_formletter(data) else: print "Got comment from %(name)s, skipping printing." % vars(data)
The first few lines of the main program show the power of a
well-designed framework. The first line is a standard import
statement, except that it’s worth noting that
win32com is a package, not a module. It is, in
fact, a collection of subpackages, modules, and functions. We need
two things from
the
win32com
package: the Dispatch
function in the
client
module, a function that allows us to
“dispatch” functions to other objects (in this case COM
servers), and the constants
submodule of the same
module, which holds the constants defined by the COM objects we want
to talk to.
The second line simply defines a variable that contains the name of
the COM server we’re interested in. It’s called
Word.Application.8
,
as you can find out from using a COM browser or reading Word’s
API (see the sidebar Finding Out About COM Interfaces).
Let’s focus now on the if
_
_name
__ == '
_
_main
__'
block, which is the
next statement after the class and function definitions.
The first task is to read the data. We import the
os
and
pickle
modules for fairly obvious reasons, and
then three references from the feedback
module we
just wrote: the DIRECTORY
where the data is stored
(this way if we change it in feedback.py, this
module reflects the change the next time it’s run), and the
FormData
and FeedbackData
classes. The next line creates an instance of the
Word
class; this opens a connection with the Word
COM server, starting the server if needed.
The for
loop is a simple iteration over the files
in the directory with all the saved files. It’s important that
this directory contain only the pickled instances, since we’re
not doing any error checking. As usual we should make the code more
robust, but we’ve ignored stability for simplicity.
The first line in the for
loop does the
unpickling. It uses the load
function from the
pickle
module, which takes a single argument, the
file which is being unpickled. It returns as many references as were
stored in the file—in our case, just one. Now, the data that
was stored was just the instance of the
FeedbackData
class. The definition of the class
itself isn’t stored in the pickled file, just the instance
values and a reference to the class.[70]
At unpickling time, unpickling instances automatically causes an
import of the module in which the class was defined. Why, then, did
we need to import the classes specifically? In Chapter 5, we said the name of the currently running
module is __main
__. In other words, the name of
the module in which the class is defined is _
_main
__ (even though the name of the file is
feedback.py
), and alas, importing _
_main
__ when we’re unpickling imports the
currently running module (which lives in
formletter.py
), which doesn’t contain the
definition of the classes of the pickled instances. This is why we
need to import the class definitions explicitly from the
feedback
module. If they weren’t made
available to the code calling pickle.unload
(in
either the local or global namespaces), the unpickling would fail.
Alternatively, we could save the source of the class in a file and
import it first before any of the instances, or, even more simply,
place the class definitions in a separate module that’s
imported explicitly by feedback.py
and
implicitly by the unpickling process in the
formletter.py
. The latter is the usual case, and
as a result, in most circumstances, you don’t need to
explicitly import the class definitions; unpickling the instance does
it all, “by magic.”[71]
The if
statement inside the loop is
straightforward. All that remains is to explain is the
print_formletter
function, and, of course, the
Word
class.
The print_formletter
function simply calls the
various methods of the word
instance of the
Word
class with the data extracted from the
data
instance. Note that we use the
string.split
function to extract the first name of
the user, just to make the letter more friendly, but this risks
strange behavior for nonstandard names.
In the Word
class, the __init
_
_ method appears simple yet hides a lot of work. It creates a
connection with the COM server and stores a reference to that COM
server in an instance variable app
. Now, there are
two ways in which the subsequent code might use this server:
dynamic dispatch and nondynamic
dispatch. In dynamic dispatch, Python doesn’t
“know” at the time the program is running what the
interface to the COM server (in this case Microsoft Word) is. It
doesn’t matter, because COM allows Python to interrogate the
server and determine the protocol, for example, the number and kinds
of arguments each function expects. This approach can be slow,
however. A way to speed it up is to run the
makepy.py
program, which does this once for each
specified COM server and stores this information on disk. When a
program using that specific server is executed, the dispatch routine
uses the precomputed information rather than doing the dynamic
dispatch. The program as written works in both cases. If
makepy.py was run on Word in the past, the fast
dispatch method is used; if not, the dynamic dispatch method is used.
For more information on these issues, see the information for the
win32
extensions at http://www.python.org/windows/win32all/.
To explain the Word
class methods, let’s
start with a possible template document, so that we can see what
needs to be done to it to customize it. It’s shown in Figure 10.3.
As you can see, it’s a pretty average document, with the
exception of some text in between %
signs.
We’ve used this notation just to make it easy for a program to
find the parts which need customization, but any other technique
could work as well. To use this template, we need to open the
document, customize it, print it, and close it. Opening it is done by
the open
method of the Word
class. The printing and closing are done similarly. To customize, we
replace the %name%
,
%firstname%
, and %address%
text
with the appropriate strings. That’s what the
replace
method of the Word
class does (we won’t cover how we figured out what the exact
sequence of calls should be; see Finding Out About COM Interfaces for details).
Putting all of this at work, the program, when run, outputs text like:
C:Programs> python formletter.py
Printing letter for John Doe. Got comment from Your Mom, skipping printing. Printing letter for Susan B. Anthony.
and prints two customized letters, ready to be sent in the mail. Note that the Word program doesn’t show up on the desktop; by default, COM servers are invisible, so Word just acts behind the scenes. If Word is currently active on the desktop, each step is visible to the user (one more reason why it’s good to run these things after hours).
[69] Setting up this kind of automatic regularly scheduled program
is easily done on most platforms, using, for example,
cron
on Unix or the AT
scheduler on Windows NT.
[70] There are very good reasons for this behavior: first, it reduces the total size of pickled objects, and more importantly, it allows you to unpickle instances of previous versions of a class and automatically upgrade them to the newer class definitions.
[71] This point about
pickling of top-level classes is a subtle one; it’s much beyond
the level of this book. We mention it here because 1) we need to
explain the code we used, and 2) this is about as complex as Python
gets. In some ways this should be comforting—there is really no
“magic” here. The apparently special-case behavior of
pickle
is in fact a natural consequence of
understanding what the __main
__ module
is.