Sunday, 24 February 2019

VBA - Python - COM Interoperability error when you forget error handler

I have been writing quite a lot of Python using the gateway class pattern. Python does COM inter-operability well including throwing errors with rich error info. However, do remember to provide an error handler or you get a strange error message.

Background: Error Handling in COM

I sketch the briefest of details below on HRESULT and ICreateErrorInfo but for a fuller read see this link.

HRESULT

Consider the following extract from the IDL of the Microsoft Scripting Runtime, specifically the Dictionary's RemoveAll method. (There is no return value to confuse matters). Understand that the HRESULT signals the success or failure of the method invocation. An HRESULT is a 32-bit integer, if it is zero (symbollically S_OK) then the method succeeded but any other value is an error, so 32-bits can support billions or error numbers. You can see how this 'error space' is divided at this link.

interface IDictionary : IDispatch {

        [id(0x00000008), helpstring("Remove all information from the dictionary."), helpcontext(0x00214b41)]
        HRESULT RemoveAll();

ICreateErrorInfo interface

Nevertheless, VBA programmers are used to error description strings and the HRESULT does not carry this information. Instead, an ErrorInfo object is created by calling the ICreateErrorInfo there you can see the error Description being settable. Python supports rich error handling via ICreateErrorInfo.

Code

So some code we illustrate..

Python Class

So the following Python code is a class with two methods, the first method PythonThrowsAnEror results in Python throwing an error because we try to call a method that does not exist. In the second method, we throw a custom error; this could be a business logic error as well as a system error.

class ThrowsErrors(object):
    _reg_clsid_ = "{FD538AF6-6B9C-4E53-8013-93D74665F23E}"
    _reg_progid_ = 'PythonComTypes.ThrowsErrors'
    _public_methods_ = ['PythonThrowsAnEror','ThrowMyOwnError']

    def PythonThrowsAnEror(self):
        a=self.noexist()

    def ThrowMyOwnError(self):
        a=2+2
        raise COMException(description="Throw an error!", scode=winerror.E_FAIL, source = "ThrowsErrors")

if __name__ == '__main__':
    print("Registering COM servers...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(ThrowsErrors)


VBA Test Client code

So here is some test code but you must run the above Python class first to register it!

Sub TestThrowsErrors()
    On Error GoTo PythonErrHand
    Dim obj As Object
    Set obj = VBA.CreateObject("PythonComTypes.ThrowsErrors")
    Debug.Print obj.PythonThrowsAnEror
    'Debug.Print obj.ThrowMyOwnError
SingleExit:
    Exit Sub
PythonErrHand:
    Debug.Print err.Description, Hex$(err.Number), err.source
    
End Sub

Running the code prints the following (edited) in the Immediate window...

Unexpected Python Error: Traceback (most recent call last):
  File "C:\PROGRA~2\MICROS~4\Shared\PYTHON~1\lib\site-packages\win32com\server\policy.py", line 278, in _Invoke_
    return self._invoke_(dispid, lcid, wFlags, args)
  File "C:\PROGRA~2\MICROS~4\Shared\PYTHON~1\lib\site-packages\win32com\server\policy.py", line 283, in _invoke_
    return S_OK, -1, self._invokeex_(dispid, lcid, wFlags, args, None, None)
  File "C:\PROGRA~2\MICROS~4\Shared\PYTHON~1\lib\site-packages\win32com\server\policy.py", line 586, in _invokeex_
    return func(*args)
  File "N:\source\repos\ThrowsErrors\ThrowsErrors\ThrowsErrors.py", line 10, in PythonThrowsAnEror
    a=self.noexist()
AttributeError: 'ThrowsErrors' object has no attribute 'noexist'
              80004005      Python COM Server Internal Error

You can see that Python is passing a whole error stack via the Err.Description field. For the error number it is using &H80004005 which is E_FAIL which signifies a general error.

If &H80004005 is the favoured catch all error number for errors then we can do the same for our custom errors. In the Python code above one can see in the ThrowMyOwnError() method we also use E_FAIL. If you uncomment the second method call (and comment the first to suppress it) then the test code now prints

Throw an error!             80004005      ThrowsErrors

So that's fine but there is one last gotcha.

What happens if I forget my error handler?

Of course, if you write production quality code you'd add an error handler for every single Sub and Function! But if you are playing with some test code you may forget to add an error handler, let's simulate this by commenting out the On Error Goto PythonErrHand line of code. If you then run the code you get the following message box...

and so this is devoid of any rich error information. So just be aware.

No comments:

Post a Comment