Hi all,

I am one of the core developers for the NVDA screen reader at
http://www.nvda-project.org/

We use comtypes very heavily in our project, mostly for access to MSAA, RichEdit, MSHTML, and SAPI speech synthesizers.

However, for access to such apps as MS Word, MS Excel etc, we tend to use pywin32, for the single reason that comtypes interfaces for these apps can take so long to generate, plus pywin32 has very good dynamic IDispatch support.

Pywin32 is great, but it is annoying switching between python COM implementations, we would rather just use one or the other.

This is the reason why I have created a patch for latest comtypes trunk, which improves its support for dynamic IDispatch, making it now possible to fully depend only on dynamic IDispatch, with out having to generate any interfaces when going from one IDispatch object to another etc.

Firstly, a variable called 'disableCodeGeneration' has been added to the base comtypes package. If you do not wish for any COM interfaces to be generated by comtypes if it has to, set this to True. It is False by default.

comtypes.client._generate._createWrapper now throws an exception, if disableCodeGeneration is True and it finally gets to the point where it must generate some interfaces (they didn't exist already or they are out of date).

comtypes.client.GetBestInterface now tries returning a dynamic.Dispatch object if at any time there is an error. e.g. the interface couldn't be found or created etc.

So with just these changes, if you have no already generated interfaces, and you make a dynamic Dispatch object some how, now if any of its methods or properties return IDispatch objects, rather than them being smartly wrapped in real generated interfaces, dynamic Dispatch objects are just returned instead.

So the rule is: with disableCodeGeneration set to True, if the interface already exists it will use it, but if it doesn't and it supports IDispatch, it will use dynamic Dispatch, rather than trying to generate proper interfaces.

I also made some changes to comtypes.client.dynamic._Dispatch, so that it works nicer with properties and methods.

It now makes use of ITypeComp, using Bind to get IDs and invKind info from a name. It also caches as much information as it can to make sure its as fast as possible.

Note that information is cached by its guid and name, on the actual dynamic Dispatch class, so that multiple uses of the same IDispatch type stay just as fast.

So now, the only time an IDispatch property is treeted as a property (i.e. doesn't have to be called like a function) is if its invkind is propget and it has no params or optional params. Otherwise the method caller is used instead.

examples in MS Word are:
range.text is a property
range.collapse() is a method
examples in MSAA are:
pacc.accName() or pacc.accName(0) The property has an optional argument so therefore it has to be treeted as a method. pacc.accFocus is a property with no arguments at all, so it can be treeted as a property.

__call__ has also been added to _Dispatch, so that the IDispatch's default method can be called, by calling the dynamic Dispatch object like a function. An example of this is an MS Word paragraphs object, calling paragraphs(1) will now work.

Also __enum now tries querying directly to IEnumVARIANT, if it fails to call _enum. This now means that MSAA objects that have an IEnumVARIANT as as one of their interfaces will now work pythonicly as a list/iterater.

This patch makes dynamic._Dispatch rely on the fact that the IDispatch object has ITypeInfo via GetTypeInfo(0). The question i have is is this true for all IDispatch objects? or should we still support the more basic GetIdsOfNames and then just treet all properties as methods, if ITypeComp can't be found?

A completely different approach to this could be where if a dynamic Dispatch object calls invoke, and invoke returns an IDispatch, then we should wrap it as a dynamic Dispatch object rather than getting its best interface. However, I am not sure if this could currently be at all possible in comtypes as we would need to be able to detect the fact that invoke was called from a dynamic Dispatch object, as its result gets wrapped... at a very low level. I think for now, allowing the user to refuse code generation better suits the pattern of use.

What are people's thoughts on this?

Mick


Index: comtypes/__init__.py
===================================================================
--- comtypes/__init__.py        (revision 64571)
+++ comtypes/__init__.py        (working copy)
@@ -2,6 +2,9 @@
 
 __version__ = "0.5.0a"
 
+disableCodeGeneration=False #Set to true to not allow generating interface 
modules and to use dynamic IDispatch instead
+
+
 from ctypes import *
 from _ctypes import COMError
 from comtypes import partial
Index: comtypes/client/_generate.py
===================================================================
--- comtypes/client/_generate.py        (revision 64571)
+++ comtypes/client/_generate.py        (working copy)
@@ -197,6 +197,8 @@
                 if file_:
                     file_.close()
 
+    if comtypes.disableCodeGeneration:
+        raise RuntimeError("Not allowed to generate module")
     # generate the module since it doesn't exist or is out of date
     from comtypes.tools.tlbparser import generate_module
     if comtypes.client.gen_dir is None:
Index: comtypes/client/__init__.py
===================================================================
--- comtypes/client/__init__.py (revision 64571)
+++ comtypes/client/__init__.py (working copy)
@@ -122,13 +122,16 @@
         punk.QueryInterface(comtypes.IUnknown, typeattr.guid)
     except comtypes.COMError, details:
         logger.debug("Does not implement default interface, returning dynamic 
object")
-        return comtypes.client.dynamic.Dispatch(punk)
+        return 
dynamic.Dispatch(punk.QueryInterface(comtypes.automation.IDispatch))
 
     itf_name = tinfo.GetDocumentation(-1)[0] # interface name
     tlib = tinfo.GetContainingTypeLib()[0] # typelib
 
     # import the wrapper, generating it on demand
-    mod = GetModule(tlib)
+    try:
+        mod = GetModule(tlib)
+    except:
+        return 
dynamic.Dispatch(punk.QueryInterface(comtypes.automation.IDispatch))
     # Python interface class
     interface = getattr(mod, itf_name)
     logger.debug("Implements default interface from typeinfo %s", interface)
@@ -144,9 +147,13 @@
     #
     # Could the above code, as an optimization, check that QI works,
     # *before* generating the wrapper module?
-    result = punk.QueryInterface(interface)
-    logger.debug("Final result is %s", result)
-    return result
+    try:
+        result = punk.QueryInterface(interface)
+        logger.debug("Final result is %s", result)
+        return result
+    except:
+        return 
dynamic.Dispatch(punk.QueryInterface(comtypes.automation.IDispatch))
+
 # backwards compatibility:
 wrap = GetBestInterface
 
Index: comtypes/client/dynamic.py
===================================================================
--- comtypes/client/dynamic.py  (revision 64571)
+++ comtypes/client/dynamic.py  (working copy)
@@ -34,28 +34,25 @@
         self._obj = _obj
         
     def __call__(self, *args):
-        return self._obj._comobj.Invoke(self._id, *args)
+        return self._obj._comobj.Invoke(self._id, 
*args,**dict(_invkind=comtypes.automation.DISPATCH_METHOD|comtypes.automation.DISPATCH_PROPERTYGET))
 
-    def __getitem__(self, *args):
-        return self._obj._comobj.Invoke(self._id, *args,
-                                        
**dict(_invkind=comtypes.automation.DISPATCH_PROPERTYGET))
-
-    def __setitem__(self, *args):
-        if _is_object(args[-1]):
-            self._obj._comobj.Invoke(self._id, *args,
-                                        
**dict(_invkind=comtypes.automation.DISPATCH_PROPERTYPUTREF))
-        else:
-            self._obj._comobj.Invoke(self._id, *args,
-                                        
**dict(_invkind=comtypes.automation.DISPATCH_PROPERTYPUT))
-
 class _Dispatch(object):
     # Expose methods and properties via fully dynamic dispatch
+
+    __memberCache={} #key is tuple of guid,memberName and value is funcDesc
+
     def __init__(self, comobj):
         self.__dict__["_comobj"] = comobj
         self.__dict__["_ids"] = {} # Tiny optimization: trying not to use 
GetIDsOfNames more than once
+        typeInfo=self._comobj.GetTypeInfo(0)
+        self.__dict__['_iid']=typeInfo.GetTypeAttr().guid
+        
self.__dict__['_typeComp']=typeInfo.QueryInterface(comtypes.typeinfo.ITypeComp)
 
     def __enum(self):
-        e = self._comobj.Invoke(-4) # DISPID_NEWENUM
+        try:
+            e = self._comobj.Invoke(-4) # DISPID_NEWENUM
+        except:
+            e=self._comobj
         return e.QueryInterface(comtypes.automation.IEnumVARIANT)
 
     def __getitem__(self, index):
@@ -68,45 +65,41 @@
             raise IndexError, "index out of range"
         return item
 
+    def __call__(self,*args):
+        return self._comobj.Invoke(0, 
*args,**dict(_invkind=comtypes.automation.DISPATCH_PROPERTYGET|comtypes.automation.DISPATCH_METHOD))
+
     def QueryInterface(self, *args):
         "QueryInterface is forwarded to the real com object."
         return self._comobj.QueryInterface(*args)
 
     def __getattr__(self, name):
-##        tc = 
self._comobj.GetTypeInfo(0).QueryInterface(comtypes.typeinfo.ITypeComp)
-##        dispid = tc.Bind(name)[1].memid
-        dispid = self._ids.get(name)
-        if not dispid:
-            dispid = self._comobj.GetIDsOfNames(name)[0]
-            self._ids[name] = dispid
-        
-        flags = comtypes.automation.DISPATCH_PROPERTYGET
-        try:
-            result = self._comobj.Invoke(dispid, _invkind=flags)
-        except COMError, (hresult, text, details):
-            if hresult in ERRORS_BAD_CONTEXT:
-                result = MethodCaller(dispid, self)
-                self.__dict__[name] = result
-            else: raise
-        except: raise
-        
-        return result
+        memberKey=(self._iid,name)
+        if not memberKey in self.__memberCache:
+            
self.__memberCache[memberKey]=memberDesc=self._typeComp.Bind(name)[1]
+        else:
+            memberDesc=self.__memberCache[memberKey]
+        if memberDesc.invkind==comtypes.automation.DISPATCH_PROPERTYGET and 
memberDesc.cParams==0 and memberDesc.cParamsOpt==0:
+            result = self._comobj.Invoke(memberDesc.memid, 
_invkind=comtypes.automation.DISPATCH_PROPERTYGET)
+        else:
+            result = MethodCaller(memberDesc.memid, self)
+        return result        
 
     def __setattr__(self, name, value):
-        dispid = self._ids.get(name)
-        if not dispid:
-            dispid = self._comobj.GetIDsOfNames(name)[0]
-            self._ids[name] = dispid
+        memberKey=(self._iid,name)
+        if not memberKey in self.__memberCache:
+            
self.__memberCache[memberKey]=memberDesc=self._typeComp.Bind(name)[1]
+        else:
+            memberDesc=self.__memberCache[memberKey]
         # First try propertyput, if that fails with
         # DISP_E_MEMBERNOTFOUND then try propertyputref
         flags = comtypes.automation.DISPATCH_PROPERTYPUT
         try:
-            return self._comobj.Invoke(dispid, value, _invkind=flags)
+            return self._comobj.Invoke(memberDesc.memid, value, _invkind=flags)
         except COMError, (hresult, text, details):
             if hresult == hres.DISP_E_MEMBERNOTFOUND: pass
             else: raise
         flags = comtypes.automation.DISPATCH_PROPERTYPUTREF
-        return self._comobj.Invoke(dispid, value, _invkind=flags)
+        return self._comobj.Invoke(memberDesc.memid, value, _invkind=flags)
 
     def __iter__(self):
         return _Collection(self.__enum())
-------------------------------------------------------------------------
Check out the new SourceForge.net Marketplace.
It's the best place to buy or sell services for
just about anything Open Source.
http://sourceforge.net/services/buy/index.php
_______________________________________________
comtypes-users mailing list
comtypes-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/comtypes-users

Reply via email to