Skip to content

Drop comtypes requirement #1443

@wkschwartz

Description

@wkschwartz

On Windows, xlwings has required comtypes since at least version v0.9.0 (588f0f5) until at least the current master (dceeedd). After looking at it carefully, I think this may be unnecessary. The cost of requiring an unnecessary library is added time in installation under normal set ups and added complexity and size in packaging and distribution (see, e.g., #625, which is quite similar to my use case). Very shortly I will send a PR to accomplish my recommended changes.

The only imports from comtypes are IUnknown and IDispatch:

from comtypes import IUnknown
from comtypes.automation import IDispatch

IUnknown

IUnknown is only used in comtypes_to_pywin() in case the interface argument is not set:

def comtypes_to_pywin(ptr, interface=None):
_PyCom_PyObjectFromIUnknown = PyDLL(pythoncom.__file__).PyCom_PyObjectFromIUnknown
_PyCom_PyObjectFromIUnknown.restype = py_object
if interface is None:
interface = IUnknown
return _PyCom_PyObjectFromIUnknown(ptr, byref(interface._iid_), True)

But comtypes_to_pywin is only called once, in get_xl_app_from_hwnd(), with the interface argument set to IDispatch.

def get_xl_app_from_hwnd(hwnd):
pythoncom.CoInitialize()
child_hwnd = win32gui.FindWindowEx(hwnd, 0, 'XLDESK', None)
child_hwnd = win32gui.FindWindowEx(child_hwnd, 0, 'EXCEL7', None)
ptr = accessible_object_from_window(child_hwnd)
p = comtypes_to_pywin(ptr, interface=IDispatch)
disp = COMRetryObjectWrapper(Dispatch(p))
return disp.Application

Thus

if interface is None:
interface = IUnknown
return _PyCom_PyObjectFromIUnknown(ptr, byref(interface._iid_), True)

could be replaced as

    return _PyCom_PyObjectFromIUnknown(ptr, byref(IDispatch._iid_), True)

with comtypes_to_pywin no longer taking an interface argument.

So the import of IUnknown can be removed completely without changing a thing.

IDispatch

IDispatch is used only in get_xl_app_from_hwnd() as shown above and from accessible_object_from_window():

def accessible_object_from_window(hwnd):
ptr = POINTER(IDispatch)()
res = oledll.oleacc.AccessibleObjectFromWindow(
hwnd, OBJID_NATIVEOM,
byref(IDispatch._iid_), byref(ptr))
return ptr

(accessible_object_from_window() is called only from get_xl_app_from_hwnd() as shown above and from is_hwnd_xl_app(), where nothing is done with the returned pointer.)

So it looks like there is one use of ctypes.POINTER(IDispatch) and there are two uses of ctypes.byref(IDispatch._iid_).

ctypes.POINTER(IDispatch)

Correct me if I'm wrong, but I believe this declares a pointer to a structure, and, from the code defining IDispatch, it looks like the structure has no _fields_. IDispatch inherits from our friend IUnknown, which also defines no _fields_. (The metaclass for IUnknown is defined here; notice that IUnknown._case_insensitive_ is set to False.)

Moreover, the pointer produced by POINTER(IDispatch) is only ever passed directly to oleacc.AccessibleObjectFromWindow (shown above) and to pythoncom.PyCom_PyObjectFromIUnknown. Being C++ functions and being called through ctypes suggests to me that AccessibleObjectFromWindow and PyCom_PyObjectFromIUnknown really only care about the memory address contained in the pointer and, for the byref use, the memory address at which it is stored, not any of its Python metadata it might gain by coming from POINTER(IDispatch) rather than ctypes.c_void_p. I.e., it's not like there's any type checking happening.

So I would argue that POINTER(IDispatch)() can be replaced with ctypes.c_void_p() and a comment linking to IDispatch's documentation.

ctypes.byref(IDispatch._iid_)

IDispatch._iid_ is defined here. Can this be replaced with the following? It's just the essentials from here.

import ctypes

class _GUID(ctypes.Structure):
    # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/49e490b8-f972-45d6-a3a4-99f924998d97
    _fields_ = [
        ("Data1", ctypes.c_ulong),
        ("Data2", ctypes.c_ushort),
        ("Data3", ctypes.c_ushort),
        ("Data4", ctypes.c_byte * 8)]

_IDISPATCH_GUID = GUID()
ctypes.oledll.ole32.CLSIDFromString("{00020400-0000-0000-C000-000000000046}", ctypes.byref(_IDISPATCH_GUID))

Then the two instances of byref(IDispatch._iid_) could be replaced with byref(_IDISPATCH_GUID).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions