27 May 2011

Python, OneNote & You

So I wanted to write a COM extension for OneNote the other day, and I was looking through the developer documents on MSDN, and noticed that all the code samples were in C#. Now, I’ve got a bit of a soft spot for dynamically typed languages and I was wondering if I could use Ruby or Python instead.

It turns out that Python actually has a robust Win32COM library that allows you to interface with Office applications, but I couldn’t find any documentation specifically for OneNote. Fortunately, like the other Office apps, most of the OneNote C# interfaces are accessible from the Win32COM api as defined in the documentation, with a few slight changes to account for the different syntax. So I’ve put together a basic tutorial to get started with writing Python extensions for OneNote.

If you don’t have Python or PyWin32 already, go ahead and down the installers. I assume you’re developing on Windows or at least doing all this inside of a Windows VM

Setting up your Python Environment

  • Get the latest build of Python from here [I chose 3.2, x64, Windows]
  • Get a matching build of PyWin32 from here

PyWin32 comes bundled with a little app called PythonWin that helps you write code and view its output in a pretty straightforward way, I would recommend using this if you don’t have a preferred IDE. Personally, I like Sublime Text , and I have it configured to compile Python code.

You may also want to add Python to your PATH executable so that you can run it from the command line. To do this, right click My Computer –> Properties –> Advanced System Settings –> Environment Variables, select Path and Edit. You want to add the Python and the Python\Scripts folder to your PATH variables. For instance, I added

C:\Python32; C:\Python32\Scripts;

to the end of the variable value field.

Interacting with the OneNote Object Model

Go ahead and create a new file called onenote.py , and open it up in your editor of choice.

Now, we need to interact with OneNote’s object model to update notebooks, and here we run into our first problem – how do we teach Python what interfaces are available to it via the OneNote OM? In C# you would do something like

using Microsoft.Office.Interop.OneNote;
OneNote.Application oneNoteApp = new OneNote.Application();

Well in Python, you can get started by importing the win32com client library with the following statement:

import win32com.client

But Python, unlike C#, is a dynamic language that does not need to be pre-compiled. The COM architecture was primarily built to support lower level languages like C & C++, where you can import and compile headers. Since Python is dynamically typed, we need to handle this differently. Fortunately the win32com library has a few tricks up its sleeve to deal with this. There are two ways you can go about telling Python to learn the interface – dynamically or statically, also known as late binding & early binding.

Dynamic Proxy

In late binding, the Python app starts off with no understanding of the OneNote object model, except that it is a basic COM object. When an object method is executed, it queries the object for the property and if it exists, completes the operation. For instance, if you were to call

height = foo.getHeight

Python queries foo, checks if it has a getHeight property, and then retrieves the value and stores it in height. Querying the OM each time you execute a new command can add some overhead, so unless you’re building a really simple extension this may not be the route for you. If choose to use dynamic proxies, you can create a proxy by adding:

oneNoteApp = win32com.Dispatch('OneNote.Application')

Static Proxy

In early binding, Python creates the whole interface before executing the statements individually. This is accomplished by using MakePy, which is a utility that generates a Python source file that contains all the class definitions and interfaces for working with the OneNote Object Model. As expected this is much faster than a dynamic proxy, but requires that you bundle the supporting makepy OneNote model file with your script. If you are building a relatively simple extension, the performance increase is probably negligible.

Run the makepy script to generate the right models:

  • Navigate to \Python32\Lib\site-packages\win32com\client
  • Run makepy.py and in the dialog select Microsoft OneNote 14.0 library

makepy

Now you’re all set and you can call your static proxy in the app. You directly reference the makepy lib file if you want, but I find that the cleanest way to use it is to call gencache.EnsureDispatch() This function will check for a static proxy – if it does not exist, it will be generated. I find this a pretty painless way of handling error cases. Add the following to your code to create a static proxy.

oneNoteApp = win32com.client.gencache.EnsureDispatch('OneNote.Application')

Getting Object Info

Ok, now that we have our COM objects defined we need to start interacting with them – let’s start off by querying the app for a list of all notebooks that are open. Looking at the API for OneNote 2010 on MSDN, we would need to call the GetHierarchy method, whose interface looks like this:

HRESULT GetHierarchy(          
    [in]BSTR bstrStartNodeID,         
    [in]HierarchyScope hsScope,        
    [out]BSTR * pbstrHierarchyXmlOut,       
    [in,defaultvalue(xs2010)]XMLSchema xsSchema);

An example of how you would write this in C# is given on MSDN as:

oneNoteApp.GetHierarchy(null, OneNote.HierarchyScope.hsNotebooks, out strXML);

In Python, you would write this as:

oneNoteApp.GetHierarchy("",win32com.client.constants.hsNotebooks, result)

You’ll notice that it maps pretty well to Python syntax – the only changes are that we don’t need to declare result explicitly as an out and that we pass in “” as a parameter instead of null. For those wondering, this first parameter dictates the root node (notebook, section or page) – i.e. it will fetch all nodes that are children of that node. Leaving it blank will simply get everything. Now your code should look something like:

import win32com.client
oneNoteApp = win32com.client.gencache.EnsureDispatch('OneNote.Application')
oneNoteApp.GetHierarchy("",win32com.client.constants.hsNotebooks, result)
print(result)

Once you run this successfully, you should see a list of Notebooks formatted in the OneNote XML Schema

Next Steps

You now have all the objects defined in XML, with Notebooks, Sections & Pages as individual nodes. Once you import an XML parser library like cElementTree or MiniDom you can modify the objects and send them back to the onenote.exe process via the other methods listed in the MSDN dev docs. I’ll try to write up another blog post that explores the OneNote Object Model in a little more depth.

I’ve glossed over some of the intricacies of gencache and dynamic proxies in this post – for a more thorough overview, check out this excellent article from the O’Reilly book Python: Programming on Win32.