mhammond / pywin32

Python for Windows (pywin32) Extensions
4.99k stars 792 forks source link

win32com object methods give "Member not found" when comtypes call succeeds #1896

Open bennyrowland opened 2 years ago

bennyrowland commented 2 years ago

I have a COM library (SMISEventHandler) with which I would like to interface using win32com, which exports 2 classes (ImgEvents and SMISEvents). When I create an instance of one of the classes using EnsureDispatch("SMISEventHandler.ImgEvents") I get an object with the correct UUIDs and interface etc., the typelib is correctly passed to give the gencache interface files, and everything seems ok, but when I try to call some of the methods of the interface I get errors like:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\Public\Miniconda3\envs\hq_clin1\lib\site-packages\win32com\gen_py\2F06A879-D4E9-482E-AC01-E2B14A961C45x0x3x0\IDualImgEventTrigger.py", line 46, in ImageCreated
    return self._oleobj_.InvokeTypes(1610809349, LCID, 1, (24, 0), ((8, 1), (8, 1), (8, 1), (3, 1), (8, 1), (17, 1), (8, 1)),PatientId
pywintypes.com_error: (-2147352573, 'Member not found.', None, None)

or in some cases:

>>> ehw2.Status(1, "success")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\Public\Miniconda3\envs\hq_clin1\lib\site-packages\win32com\gen_py\2F06A879-D4E9-482E-AC01-E2B14A961C45x0x3x0\IDualImgEventTrigger.py", line 67, in Status
    return self._ApplyTypes_(1610809344, 1, (24, 32), ((3, 1), (8, 1), (8, 49)), 'Status', None,Status
  File "C:\Users\Public\Miniconda3\envs\hq_clin1\lib\site-packages\win32com\client\__init__.py", line 572, in _ApplyTypes_
    self._oleobj_.InvokeTypes(dispid, 0, wFlags, retType, argTypes, *args),
pywintypes.com_error: (-2147352562, 'Invalid number of parameters.', None, None)

Interestingly, it seems that the methods where we see "Invalid number of parameters" correspond to the methods which are defined on the second class (SMISEvents), but which have a different signature. Instances of that second class work correctly with no issues.

If I run the equivalent code in comtypes then the method calls succeed as expected

>>>obj = comtypes.client.CreateObject("SMISEventHandler.ImgEvents")
>>>obj.Status(1, "success")
0

The library also works correctly when accessed from VB6. This suggests that the COM library itself is working correctly and the fault lies with win32com. This also seems related to the following question from StackOverflow that was never resolved (other than to use comtypes): https://stackoverflow.com/questions/13238269/member-not-found-error-using-win32com

comtypes is not a great solution for me in practice because I need the cross-thread marshalling abilities of win32com, so I am very keen to find a solution to this problem, but running out of ideas to try - if anybody can suggest some ways to get a better idea of what is going on, I will certainly try them.

mhammond commented 2 years ago

It's not obvious to me what's going on. In that second call:

return self.ApplyTypes(1610809344, 1, (24, 32), ((3, 1), (8, 1), (8, 49)), 'Status', None,Status

That description of the args here is the ((3, 1), (8, 1), (8, 49)

So 3 params - There's 2 "in" params (VT_I4(3), VT_BSTR(8)) followed by a VT_BSTR with flags PARAMFLAGS_FIN | PARAMFLAG_FOPT | PARAMFLAG_FHASDEFAULT - so an optional string.

Does IDualImgEventTrigger.py have that 3rd param?

which are defined on the second class (SMISEvents)

I wonder if self._oleobj_ is "wrong"? Ie, it thinks it's to the wrong interface. I'm not sure how to check that.

A "dumb dispatch" object might work better in some ways for you - eg, in the above example, win32com will be calling the com method with those 3 args, handling the 3rd silently. A dumb dispatch called with 2 params will only supply 2 params. I'm not sure what typeinfo support comtypes has, but it might be quite similar to this.

bennyrowland commented 2 years ago

Hi @mhammond, thanks for the input. Yes, the generated file correctly picks up the third param and will supply a "not provided" value if it isn't given. Note that I want to call it with 3 arguments, I don't want to use the default value. If I call it with 0 or 1 parameter supplied, I will get an error saying that a required parameter is not provided, and if I call it with 4 or more parameters I will get an error that I am providing too many parameters, but calling it correctly gives the Invalid Number of Parameters error.

I think I agree with you that somehow the oleobj is getting misconfigured, making the ImgEvents call to an interface oleobj thinks is SMISEvents and therefore rejecting the call, even though the object actually is ImgEvents. I don't know how to fix that though.

I tried using win32com.client.dynamic.Dispatch instead of EnsureDispatch, I assume that is what you mean by "dumb dispatch", but that doesn't make any difference, I still get the same Invalid number of parameters error. I don't know how to force a "dumber" version of Dispatch than that. Using comtypes I can call it successfully using either 2 or 3 arguments, it is not succeeding there because the signature is wrong - also the other members which are not found in win32com are available through comtypes.

mhammond commented 2 years ago

win32com.client.Dispatch still attempts to use the typeinfo at runtime, so is likely to suffer the same problem as the pre-generated bindings. There is a win32com.client.dynamic.DumbDispatch that doesn't try and use any typeinfo so might help.

The other thing you could try is working out what the correct interface is, use win32com.client.gencache.GetModuleForProgID to get the generated module, and instantiate the wrapper class by passing ob._oleobj_ to the constructor.

mhammond commented 2 years ago

ie, something like:

let wrong = ... whatever you are doing now.
let mod = win32com.client.gencache.GetModuleForProgID(...)
let correct = mod.CorrectInterfaceName(wrong._oleobj_)
bennyrowland commented 2 years ago

@mhammond I tried the DumbDispatch approach which allowed me to make a call to Status() passing 2 parameters (without supplying the third as "Missing") which allowed COM to accept the function call without complaining about an incorrect number of parameters), but it then immediately crashes which I think is a result of the underlying COM object expecting 3 parameters.

The wrapper class is the one I expect it to be, and is calling the functions with the parameters that I expect them to be, I think the issue must be at the oleobj level but I have no idea how to explore that further.

DS-London commented 1 year ago

At the risk of hijacking @bennyrowland 's issue, I am seeing something similar when automating MS Outlook: what seems to be indentical code works with comtypes (and VBA) but not with win32com.

The code is adding a new Outlook Rule.

This code using comtypes works successfully and prints the expected path to the MoveTo folder (I have commented the Save() for testing)

import comtypes.client

o = comtypes.client.CreateObject('Outlook.Application')
rules = o.Session.DefaultStore.GetRules()
rule = rules.Create("Test", 0)

condition = rule.Conditions
condition.From.Recipients.Add('a.person@somewhere.com')
condition.From.Enabled=True

move = rule.Actions.MoveToFolder
root_folder = o.GetNamespace('MAPI').GetDefaultFolder(6)
dest_folder = root_folder.Folders['Test Folder']
move.Folder = dest_folder
move.Enabled = True

print(move.Folder.FolderPath)

#rules.Save()

The same code transcribed to win32com does not. The final move.Folder.FolderPath property is None.

import win32com.client as wc

o = wc.gencache.EnsureDispatch('Outlook.Application')
rules = o.Session.DefaultStore.GetRules()
rule = rules.Create("Test",0)

condition = rule.Conditions
condition.From.Recipients.Add('a.person@somewhere.com')
condition.From.Enabled=True

move = rule.Actions.MoveToFolder
root_folder = o.GetNamespace('MAPI').GetDefaultFolder(6)
dest_folder = root_folder.Folders['Test Folder']
move.Folder = dest_folder
move.Enabled = True

print(move.Folder.FolderPath) #Value is None here

#rules.Save()

It seems that the set for the Folder property is not working. The Folder object I am passing seems to be OK (I can interrogate it), but it is not 'sticking'. The same happens if I delete all the gen_py files and just use Dispatch() to create the Outlook object.

The equivalent code in VBA also runs fine.

The relevant part from the gen_py file (_MoveOrCopyRuleAction.py) is here:

class _MoveOrCopyRuleAction(DispatchBaseClass):
    CLSID = IID('{000630D0-0000-0000-C000-000000000046}')
    coclass_clsid = IID('{000610D0-0000-0000-C000-000000000046}')

    _prop_map_get_ = {
        "ActionType": (64271, 2, (3, 0), (), "ActionType", None),
        # Method 'Application' returns object of type '_Application'
        "Application": (61440, 2, (9, 0), (), "Application", '{00063001-0000-0000-C000-000000000046}'),
        "Class": (61450, 2, (3, 0), (), "Class", None),
        "Enabled": (103, 2, (11, 0), (), "Enabled", None),
        # Method 'Folder' returns object of type 'MAPIFolder'
        "Folder": (64273, 2, (9, 0), (), "Folder", '{00063006-0000-0000-C000-000000000046}'),
        "Parent": (61441, 2, (9, 0), (), "Parent", None),
        # Method 'Session' returns object of type '_NameSpace'
        "Session": (61451, 2, (9, 0), (), "Session", '{00063002-0000-0000-C000-000000000046}'),
    }
    _prop_map_put_ = {
        "Enabled": ((103, LCID, 4, 0),()),
        "Folder": ((64273, LCID, 4, 0),()),
    }

and

_MoveOrCopyRuleAction_vtables_ = [
    (( 'Application' , 'Application' , ), 61440, (61440, (), [ (16393, 10, None, "IID('{00063001-0000-0000-C000-000000000046}')") , ], 1 , 2 , 4 , 0 , 56 , (3, 0, None, None) , 0 , )),
    (( 'Class' , 'Class' , ), 61450, (61450, (), [ (16387, 10, None, None) , ], 1 , 2 , 4 , 0 , 64 , (3, 0, None, None) , 0 , )),
    (( 'Session' , 'Session' , ), 61451, (61451, (), [ (16393, 10, None, "IID('{00063002-0000-0000-C000-000000000046}')") , ], 1 , 2 , 4 , 0 , 72 , (3, 0, None, None) , 0 , )),
    (( 'Parent' , 'Parent' , ), 61441, (61441, (), [ (16393, 10, None, None) , ], 1 , 2 , 4 , 0 , 80 , (3, 0, None, None) , 0 , )),
    (( 'Enabled' , 'Enabled' , ), 103, (103, (), [ (16395, 10, None, None) , ], 1 , 2 , 4 , 0 , 88 , (3, 0, None, None) , 0 , )),
    (( 'Enabled' , 'Enabled' , ), 103, (103, (), [ (11, 1, None, None) , ], 1 , 4 , 4 , 0 , 96 , (3, 0, None, None) , 0 , )),
    (( 'ActionType' , 'ActionType' , ), 64271, (64271, (), [ (16387, 10, None, None) , ], 1 , 2 , 4 , 0 , 104 , (3, 0, None, None) , 0 , )),
    (( 'Folder' , 'Folder' , ), 64273, (64273, (), [ (16393, 10, None, "IID('{00063006-0000-0000-C000-000000000046}')") , ], 1 , 2 , 4 , 0 , 112 , (3, 0, None, None) , 0 , )),
    (( 'Folder' , 'Folder' , ), 64273, (64273, (), [ (9, 1, None, "IID('{00063006-0000-0000-C000-000000000046}')") , ], 1 , 4 , 4 , 0 , 120 , (3, 0, None, None) , 0 , )),
]

And finally this is the IDL file from the Outlook Type Library:

[
  odl,
  uuid(000630D0-0000-0000-C000-000000000046),
  helpcontext(0x0000089f),
  dual,
  oleautomation
]
interface _MoveOrCopyRuleAction : IDispatch {
    [id(0x0000f000), propget, helpcontext(0x000008a0)]
    HRESULT Application([out, retval] _Application** Application);
    [id(0x0000f00a), propget, helpcontext(0x000008a1)]
    HRESULT Class([out, retval] OlObjectClass* Class);
    [id(0x0000f00b), propget, helpcontext(0x000008a2)]
    HRESULT Session([out, retval] _NameSpace** Session);
    [id(0x0000f001), propget, helpcontext(0x000008a3)]
    HRESULT Parent([out, retval] IDispatch** Parent);
    [id(0x00000067), propget, helpcontext(0x000008a4)]
    HRESULT Enabled([out, retval] VARIANT_BOOL* Enabled);
    [id(0x00000067), propput, helpcontext(0x000008a4)]
    HRESULT Enabled([in] VARIANT_BOOL Enabled);
    [id(0x0000fb0f), propget, helpcontext(0x000008a5)]
    HRESULT ActionType([out, retval] OlRuleActionType* ActionType);
    [id(0x0000fb11), propget, helpcontext(0x000008a6)]
    HRESULT Folder([out, retval] MAPIFolder** Folder);
    [id(0x0000fb11), propput, helpcontext(0x000008a6)]
    HRESULT Folder([in] MAPIFolder* Folder);
};