How to debug win32com call in python

2019-07-13 07:22发布

In order to collect output from a logging script, I would like to use onepy to add information to a OneNote 2013 notebook. Unfortunately, the method update_page_content() provided by onepy doesn't work for me. To get a deeper understanding of the problem, I switched over to C#, where many online examples for the OneNote API exist and after some trouble I managed to get the following minimalistic C# example to work:

using System;
using OneNote = Microsoft.Office.Interop.OneNote;

class Program
{
    static void Main(string[] args)
    {
        OneNote.Application onenoteApp = new OneNote.Application();
        string xml = "<one:Page xmlns:one=\"http://schemas.microsoft.com/office/onenote/2013/onenote\" ...> ... </one:Page>";
        onenoteApp.UpdatePageContent(xml, DateTime.MinValue);
    }
}

The string xml was obtained by modification of an XML document, which was retrieved from OneNote using the GetPageContent method, as detailed in my linked previous question. The precise content of xml doesn't matter for the sake of this question, the only important thing is that above program runs without problems time after time and changes to an existing OneNote page are always performed successfully.

Going over to Python now, I tried to translate my minimalistic program without making substantial changes. My result looks as follows:

import win32com
import pytz
import datetime

onenote_app = win32com.client.Dispatch('OneNote.Application.15')
xml = "<one:Page xmlns:one=\"http://schemas.microsoft.com/office/onenote/2013/onenote\" ...> ... </one:Page>"
date = pytz.utc.localize(datetime.datetime.fromordinal(1))
onenote_app.UpdatePageContent(xml, date)

I tried to put much care into using the same values for the two variables. Of course the content of the two strings xml is identical (copy&paste). Also, according to the VS2015 debugger, both DateTime.MinValue and date refer to the same date. When I execute the python program, however, I recieve this very unhelpful error.

    135         def UpdatePageContent(self, bstrPageChangesXmlIn=defaultNamedNotOptArg, dateExpectedLastModified=(1899, 12, 30, 0, 0, 0, 5, 364, 0), xsSchema=2, force=False):
    136         return self._oleobj_.InvokeTypes(1610743816, LCID, 1, (24, 0), ((8, 1), (7, 49), (3, 49), (11, 49)),bstrPageChangesXmlIn
--> 137             , dateExpectedLastModified, xsSchema, force)
    138 
    139     _prop_map_get_ = {

com_error: (-2147352567, 'Exception occurred.', (0, None, None, None, 0, -2147213296), None)

To my understanding, both C# and Python actually use the same library to execute their calls (both called Microsoft OneNote 15.0 Object Library). So in principle both programs should work fine. If I am not mistaken in this point, I would thus assume that Python does something different in its call to the library. How can I trace further what the actual problem here is? Is there maybe a way to use the built-in Python support of Visual Studio 2015 to understand the difference between the C# and the Python code better?

2条回答
手持菜刀,她持情操
2楼-- · 2019-07-13 07:53

As jayongg is already pointing out in his answer, my problem is the date passed as an argument to the UpdatePageContent() call. However, his suggestion to pass zero as last modified date and to disable date checking as detailed in the documentation is not as straightforward as it may seem. In my answer I will detail all the pitfalls, which I encountered.

The first problem is shown in this question. If one tries to pass for example a datetime object or a plain integer to UpdatePageContent(), then an error like the following is raised.

    135         def UpdatePageContent(self, bstrPageChangesXmlIn=defaultNamedNotOptArg, dateExpectedLastModified=(1899, 12, 30, 0, 0, 0, 5, 364, 0), xsSchema=2, force=False):
    136         return self._oleobj_.InvokeTypes(1610743816, LCID, 1, (24, 0), ((8, 1), (7, 49), (3, 49), (11, 49)),bstrPageChangesXmlIn
--> 137             , dateExpectedLastModified, xsSchema, force)
    138 
    139     _prop_map_get_ = {

ValueError: astimezone() cannot be applied to a naive datetime

In particular, leaving the date argument blank and using the default value does not work as expected:

    135         def UpdatePageContent(self, bstrPageChangesXmlIn=defaultNamedNotOptArg, dateExpectedLastModified=(1899, 12, 30, 0, 0, 0, 5, 364, 0), xsSchema=2, force=False):
    136         return self._oleobj_.InvokeTypes(1610743816, LCID, 1, (24, 0), ((8, 1), (7, 49), (3, 49), (11, 49)),bstrPageChangesXmlIn
--> 137             , dateExpectedLastModified, xsSchema, force)
    138 
    139     _prop_map_get_ = {

TypeError: must be a pywintypes time object (got tuple)

Apparently some internals of the python API calls are messed up. If one follows the instructions on the onepy project page and uses makepy to pre-generte the API wrapper, then one can inspect the source files responsible for these calls, but at least for me this was not very instructive. I suspect that the InvokeTypes method tries to put the passed date into a format, which the API can understand, but the necessary requirements are not implemented in the Python wrapper itself.

Fortunately, there is a quite simple workaround. The trick is to make the passed datetime object timezone-aware. To do this, one has to localize it first. This can for example be done using the pytz module.

import datetime
import pytz

date = pytz.utc.localize(datetime.datetime.fromordinal(1))

With this knowledge the API call works, but the COM error from my question is raised. This is where jayongg's answer comes in: I have to pass zero as last modified date (or I need to know the date, when the last modification occurred, which should also work). Now the tricky question is: What is zero?

In my C# code, this date is given by DateTime.MinValue, which according to Visual Studio 2015 is equal to 0001-01-01 00:00:00. The same date in Python is datetime.datetime.fromordinal(1), but it still doesn't make the call work. I have to suspect that the information by Visual Studio is wrong or that some magic happens in between, maybe a type cast VSDate -> Int -> APIDate.

So how do I find out, which is the correct zero date to pass? Turns out the answer is already there, one just needs to know where to look. If one inspects in the Python API wrapper, there is a default value given for the argument in question:

dateExpectedLastModified=(1899, 12, 30, 0, 0, 0, 5, 364, 0)

The same can be obtained with the following snippet.

>> date = pytz.utc.localize(datetime.datetime(year=1899, month=12, day=30))
>> print(tuple(date.timetuple()))
(1899, 12, 30, 0, 0, 0, 5, 364, 0)

And voilà , passing the variable date to the call works just fine.

>> onenote_app.UpdatePageContent(xml, date)
>>

Makes perfect sense, right? I mean, which other date would you pass instead of 1899-12-30? Actually it does make some sense. This day is one day before the zero point of the Dublin Julian Date. According to the German Wikipedia article this day is used by Excel as zero point for dates, so it sound plausible that OneNote does the same.

Why the one day difference? Apparently, the year 1900 is mis-treated as leap year, which it was not. Thereby 1899-12-31 is shifted to 1899-12-30, which is what the API wants. Sigh. Why are there so many different agreements on how to store dates? Oh, just to mention. Office for Mac uses 1904 as zero point. Yeah, of course.

查看更多
Emotional °昔
3楼-- · 2019-07-13 08:00

You error code (-2147213296) is 0x80042010, which is:

hrLastModifiedDateDidNotMatch

0x80042010

The last modified date does not match.

https://msdn.microsoft.com/en-us/library/office/ff966472(v=office.14).aspx

You can try passing a last modified date of 0. From: https://msdn.microsoft.com/en-us/library/office/gg649853(v=office.14).aspx: dateExpectedLastModified—(Optional) The date and time that you think the page you want to update was last modified. If you pass a non-zero value for this parameter, OneNote proceeds with the update only if the value you pass matches the actual date and time the page was last modified. Passing a value for this parameter helps prevent accidentally overwriting edits users made since the last time the page was modified.

查看更多
登录 后发表回答