Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop comtypes requirement #1443

Closed
wkschwartz opened this issue Oct 30, 2020 · 0 comments · Fixed by #1444
Closed

Drop comtypes requirement #1443

wkschwartz opened this issue Oct 30, 2020 · 0 comments · Fixed by #1444
Milestone

Comments

@wkschwartz
Copy link
Contributor

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

wkschwartz added a commit to wkschwartz/xlwings that referenced this issue Oct 30, 2020
wkschwartz added a commit to wkschwartz/xlwings that referenced this issue Oct 30, 2020
@fzumstein fzumstein added this to the 0.20.9 milestone Oct 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants