[ Team LiB ] |
28.2 Interfacing with COM: Cheap Public RelationsAt this point, we have a program that is run whenever users fill in the feedback form and that writes out instances of the feedback data to files on the server. We'll use this data to do two things. First, a program that's run periodically (say, at 2 a.m., every night)[2] will look through the saved data, find out which saved pickled files correspond to complaints, and print out a customized letter to the complainer. The second use we'll make of that data is a GUI browser to look through the stored feedback entries. All this 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, 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 Component Object Model (COM). COM is, among other things, a standard for interaction between programs, which allows COM-compliant programs to talk to, access the data in, and execute commands in other COM-compliant programs. Roughly speaking, the program doing the calling is called a COM client, and the program doing the executing is called a COM server. All major Microsoft products are COM-aware and can act as servers. Microsoft Word is one of these, and the one we'll use here, since 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, on Windows. Mark Hammond and Greg Stein have made available a set of extensions to Python for Windows called win32com, which 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 tasks, which is also the simplest. The basic tasks that our form letter generator program needs to do are:
This task is almost as simple to express in Python with win32com. Here's a program called formletter.py: from win32com.client import gencache, constants WORD = 'Word.Application' False, True = 0, -1 class Word: def __init__(self): self.app = gencache.EnsureDispatch(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+"%" 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:\David\Book\tofutemplate.doc") word.replace("name", data.name) word.replace("address", data.address) word.replace("firstname", data.name.split( )[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 EnsureDispatch function in the gencache module, a function that allows us to dispatch functions to other objects (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, as you can find out from using a COM browser or reading Word's API (see the sidebar "Finding Out About COM Interfaces"). Using gencache.EnsureDispatch ensures that late binding is used for the Word library, and also ensures that all Word related constants are loaded. 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 because we're going to need functions they define, 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 necessary. 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. (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. 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. This design 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. The if statement inside the loop is straightforward. 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 split method 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. That's not a problem because COM allows Python to query the server and determine the number and kinds of arguments each function expects. To explain the Word class methods, let's start with a possible template document, shown in Figure 28-3. so that we can see what needs to be done to it to customize it. Figure 28-3. Joe's template letter to complainersAs 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 that 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 the sidebar 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. |
[ Team LiB ] |