I l@ve RuBoard Previous Section Next Section

10.2 Interfacing with COM: Cheap Public Relations

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[3] ) 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.

[3] 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.

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:

  1. Open all of the pickled files in the appropriate directory and unpickle them.

  2. 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.

  3. Open a Word document containing a template of the letter we want to send, and fill in the appropriate pieces with the customized information.

  4. 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:\David\Book\tofutemplate.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梚n 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.[4]

[4] 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.

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."[5]

[5] 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梩here 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.


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.

Figure 10.3. Joe's template letter to complainants
figs/lpy_1003.gif

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).

Finding Out About COM Interfaces

How can you find out what the various methods and attributes of COM objects are? In general, COM objects are just like any other program; they should come with documentation. In the case of COM objects, however, it's quite possible to have the software without the documentation, simply because, as in the case of Word, it's possible to use Word without needing to program it. There are three strategies available to you if you want to explore a COM interface:

  • Find or buy the documentation; some COM programs have their documentation available on the Web, or available in printed form.

  • Use a COM browser to explore the objects. Pythonwin (part of the win32all extensions to Python on Windows, see Appendix B), for example, comes with a COM browser tool that lets you explore the complex hierarchy of COM objects. It's not much more than a listing of available objects and functions, but sometimes that's all you need. Development tools such as Microsoft's Visual Studio also come with COM browsers.

  • Use another tool to find what's available. For the example above, we simply used Microsoft Word's "macro recorder" facility to produce a VBA (Visual Basic for Applications) script, which is fairly straightforward to translate to Python. Macros tend to be fairly low-intelligence programs, meaning that the macro-recording facility can't pick up on the fact that you might want to do something 10 times, and so just records the same action multiple times. But they work fine for finding out that the equivalent of selecting the Print item of the File menu is to "say" ActiveDocument.PrintOut().

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).

I l@ve RuBoard Previous Section Next Section