root/MGET/Trunk/PythonPackage/src/GeoEco/COM.py

Revision 429, 67.6 kB (checked in by jjr8, 4 months ago)

Merged [428] from Jason branch into Trunk. This will be re-released as MGET 0.7b2.

Line 
1 # COM.py - Provides an infrastructure for exposing GeoEco classes as Microsoft
2 # COM Automation classes.
3 #
4 # Copyright (C) 2007 Jason J. Roberts
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License (available in the file LICENSE.TXT)
15 # for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
21 import sys
22
23 # Skip everything unless we're running on Windows.
24
25 if sys.platform.lower() == u'win32':
26
27     import datetime
28     import inspect
29     import logging
30     import new
31     import re
32     import sys
33     import types
34
35     import pythoncom
36     import pywintypes
37     import win32com.client
38     import win32com.server.dispatcher
39     from win32com.server.exception import COMException
40     import win32com.server.policy
41     import winerror
42
43     from GeoEco.DynamicDocString import DynamicDocString
44     from GeoEco.Internationalization import *
45     from GeoEco.Metadata import *
46     from GeoEco.Types import *
47
48
49     TypeLibraryGUID = '{B062A595-DFFC-4980-9212-EBC8A54A4845}'          # Do not change this unless you know what you're doing
50     TypeLibraryVersion = (0,5)                                          # TODO: Automatically generate this version number (question: when should it change?) -- Trac ticket #32
51     TypeLibraryLCID = 0                                                 # LOCALE_USER_DEFAULT, which is what pythoncom uses by default
52
53
54     class ExceptionTranslatingDispatcher(win32com.server.dispatcher.DispatcherBase):
55        
56         def __init__(self, policyClass, obj):
57             win32com.server.dispatcher.DispatcherBase.__init__(self, policyClass, obj)
58
59
60     class MetadataDesignatedWrapPolicy(win32com.server.policy.DesignatedWrapPolicy):
61        
62         def _wrap_(self, ob):
63
64             # Obtain the metadata for the class that was just instantiated by
65             # pythoncom.
66
67             classMetadata = ob.__doc__.Obj
68
69             # Initialize variables required by the base class _wrap_ method.
70             # That method will read the type library, look up the interface
71             # definition using the COM IID, and configure a wrapper that will
72             # pass COM calls to the wrapped class's methods.
73
74             ob._com_interfaces_ = [u'I' + classMetadata.Name]
75             ob._typelib_guid_ = globals()[u'TypeLibraryGUID']
76             ob._typelib_version_ = globals()[u'TypeLibraryVersion']
77             ob._typelib_lcid_ = globals()[u'TypeLibraryLCID']
78
79             # Call the base class _wrap_ to perform the wrapping.
80
81             self._Log(_(u'COM object 0x%08x: Initializing wrapper for %s.%s instance exposing interface %s (I%s) from type library %s, version %i.%i, LCID %i'), id(ob), classMetadata.Module.Name, classMetadata.Name, classMetadata.COMIID, classMetadata.Name, globals()['TypeLibraryGUID'], globals()['TypeLibraryVersion'][0], globals()['TypeLibraryVersion'][1], globals()['TypeLibraryLCID'])
82             win32com.server.policy.DesignatedWrapPolicy._wrap_(self, ob)
83
84         def _invokeex_(self, dispid, lcid, wFlags, args, kwArgs, serviceProvider):
85
86             # Generate a display string for wFlags for a log message.
87
88             wFlagsDisplayStrings = []
89
90             if wFlags & pythoncom.DISPATCH_METHOD:
91                 wFlagsDisplayStrings.append(u'DISPATCH_METHOD')
92             if wFlags & pythoncom.DISPATCH_PROPERTYGET:
93                 wFlagsDisplayStrings.append(u'DISPATCH_PROPERTYGET')
94             if wFlags & pythoncom.DISPATCH_PROPERTYPUT:
95                 wFlagsDisplayStrings.append(u'DISPATCH_PROPERTYPUT')
96             if wFlags & pythoncom.DISPATCH_PROPERTYPUTREF:
97                 wFlagsDisplayStrings.append(u'DISPATCH_PROPERTYPUTREF')
98             if len(wFlagsDisplayStrings) <= 0 or wFlags & ~(pythoncom.DISPATCH_METHOD | pythoncom.DISPATCH_PROPERTYGET | pythoncom.DISPATCH_PROPERTYPUT | pythoncom.DISPATCH_PROPERTYPUTREF) > 0:
99                 wFlagsDisplayStrings.append(_(u'unknown flags!'))
100
101             wFlagsDisplayString = u' | '.join(wFlagsDisplayStrings)
102
103             # Look up the name of the member in the base class's dictionaries.
104
105             memberName = None
106
107             if wFlags & pythoncom.DISPATCH_METHOD:
108                 try:
109                     memberName = self._dispid_to_func_[dispid]
110                 except KeyError:
111                     pass
112            
113             if memberName is None and wFlags & pythoncom.DISPATCH_PROPERTYGET:
114                 try:
115                     memberName = self._dispid_to_get_[dispid]
116                 except KeyError:
117                     pass
118            
119             if memberName is None and wFlags & (pythoncom.DISPATCH_PROPERTYPUT | pythoncom.DISPATCH_PROPERTYPUTREF):
120                 try:
121                     memberName = self._dispid_to_put_[dispid]
122                 except KeyError:
123                     pass
124
125             # If we found the member's name, look up its metadata.
126
127             if memberName is None:
128                 memberName = _(u'member not found!')
129                 memberType = u''
130                 memberMetadata = None
131             else:
132                 if not isinstance(self._obj_.__class__.__doc__, DynamicDocString) or not isinstance(self._obj_.__class__.__doc__.Obj, ClassMetadata):
133                     raise COMException(scode=winerror.DISP_E_EXCEPTION, source=_(u'GeoEco.COM.MetadataDesignatedWrapPolicy Python class'), description=_(u'Programming error in the %s class. In order for this class to expose itself to COM callers using GeoEco.COM.MetadataDesignatedWrapPolicy, it must set its __doc__ member to an instance of GeoEco.Metadata.DynamicDocString and initialize it to an instance of GeoEco.Metadata.ClassMetadata. Please contact the developer of %s for assistance.') % (self._obj_.__class__.__name__, self._obj_.__class__.__name__))
134                 classMetadata = self._obj_.__class__.__doc__.Obj
135                 if inspect.ismethod(getattr(classMetadata.Object, memberName)):
136                     if not isinstance(getattr(classMetadata.Object, memberName).__doc__, DynamicDocString) or not isinstance(getattr(classMetadata.Object, memberName).__doc__.Obj, MethodMetadata):
137                         raise COMException(scode=winerror.DISP_E_EXCEPTION, source=_(u'GeoEco.COM.MetadataDesignatedWrapPolicy Python class'), description=_(u'Programming error in the %s class. In order for this class to expose member %s to COM callers using GeoEco.COM.MetadataDesignatedWrapPolicy, it must set the __doc__ member of %s to an instance of GeoEco.Metadata.DynamicDocString and initialize it to an instance of GeoEco.Metadata.MethodMetadata. Please contact the developer of %s for assistance.') % (self._obj_.__class__.__name__, methodName, methodName, self._obj_.__class__.__name__))
138                     memberType = _(u' method')
139                 elif inspect.isdatadescriptor(getattr(classMetadata.Object, memberName)):
140                     if not isinstance(getattr(classMetadata.Object, memberName).__doc__, DynamicDocString) or not isinstance(getattr(classMetadata.Object, memberName).__doc__.Obj, PropertyMetadata):
141                         raise COMException(scode=winerror.DISP_E_EXCEPTION, source=_(u'GeoEco.COM.MetadataDesignatedWrapPolicy Python class'), description=_(u'Programming error in the %s class. In order for this class to expose member %s to COM callers using GeoEco.COM.MetadataDesignatedWrapPolicy, it must set the __doc__ member of %s to an instance of GeoEco.Metadata.DynamicDocString and initialize it to an instance of GeoEco.Metadata.PropertyMetadata. Please contact the developer of %s for assistance.') % (self._obj_.__class__.__name__, methodName, methodName, self._obj_.__class__.__name__))
142                     memberType = _(u' property')
143                 else:
144                     raise COMException(scode=winerror.DISP_E_EXCEPTION, source=_(u'GeoEco.COM.MetadataDesignatedWrapPolicy Python class'), description=_(u'Programming error in the %s class. In order for this class to expose member %s to COM callers using GeoEco.COM.MetadataDesignatedWrapPolicy, %s must be an instance method, a class method, or a property. Please contact the developer of %s for assistance.') % (self._obj_.__class__.__name__, methodName, methodName, self._obj_.__class__.__name__))
145                 memberMetadata = getattr(classMetadata.Object, memberName).__doc__.Obj
146
147             self._Log(_(u'COM object 0x%08X: InvokeEx called: dispid = %i (%s%s), lcid = %i, wFlags = %i (%s), args = %s, kwArgs = %s, serviceProvider = %s'), id(self._obj_), dispid, memberName, memberType, lcid, wFlags, wFlagsDisplayString, repr(args), repr(kwArgs), serviceProvider)
148
149             # Convert the tuple of arguments provided by pythoncom to a list, in
150             # preparation for some modifications we may need to apply to them.
151
152             args = list(args)
153             argsChanged = False
154
155             # Handle argument modifications that are specific to properties:
156
157             if isinstance(memberMetadata, PropertyMetadata):
158
159                 # Pythoncom always passes array arguments in as tuples. If the
160                 # caller wants to assign the value of a property that expects a
161                 # list, convert the argument to a list.
162
163                 if len(args) == 1 and isinstance(args[0], types.TupleType) and isinstance(memberMetadata.Type, ListTypeMetadata):
164                     args = (self._TuplesToLists(args[0], memberMetadata.Type),)
165                     argsChanged = True
166
167             # Handle argument modifications that are specific to methods:
168
169             elif isinstance(memberMetadata, MethodMetadata):
170
171                 # If this method returns multiple results, they were encoded in
172                 # IDL as [in, out] parameters that occur between the formal
173                 # input parameters ([in], [in, defaultvalue], and
174                 # [in, optional]) and the arbitrary input arbitrary argument
175                 # list ([vararg]), if one is present. We ignore these inputs and
176                 # must not pass them to the method. (Because they do not appear
177                 # in the method's definition, we would get a "too many
178                 # arguments" exception if we passed them.) We remove them from
179                 # the args list.
180
181                 if len(memberMetadata.Results) > 1:
182                     argsToRemove = len(memberMetadata.Results) - 1
183                     if memberMetadata.Arguments[len(memberMetadata.Arguments) - 1].IsArbitraryArgumentList:
184                         formalArgCount = len(memberMetadata.Arguments) - 2
185                     else:
186                         formalArgCount = len(memberMetadata.Arguments) - 1
187                     while argsToRemove > 0 and len(args) > formalArgCount:
188                         del args[formalArgCount]
189                     argsChanged = True
190                
191                 # Pythoncom always passes array arguments in as tuples. If the
192                 # caller wants to invoke a method that accepts a list(s) as an
193                 # input parameter(s), convert the argument(s) to a list(s).
194
195                 for i in range(len(memberMetadata.Arguments) - 1):
196                     if i < len(args) and memberMetadata.Arguments[i + 1].IsFormalParameter and isinstance(args[i], types.TupleType) and isinstance(memberMetadata.Arguments[i + 1].Type, SequenceTypeMetadata):
197                         args[i] = self._TuplesToLists(args[i], memberMetadata.Arguments[i + 1].Type)
198                         argsChanged = True
199
200                 # Under COM rules, an argument that is declared [in, optional] must
201                 # be a VARIANT, and the caller can pass VT_ERROR with error code
202                 # DISP_E_PARAMNOTFOUND (-2147352572) to indicate that a value is not
203                 # being supplied. From C#, for example, this is accomplished by
204                 # passing System.Type.Missing for the argument. When pythoncom
205                 # receives a VT_ERROR VARIANT, it passes us the int error code as
206                 # the argument.
207                 #
208                 # When we receive -2147352572 for an argument that has a default, we
209                 # pass the default to the method (instead of -2147352572). We do
210                 # this regardless of whether the argument was defined in the type
211                 # library as [in, defaultvalue] or [in, optional]. Pythoncom does
212                 # not appear to distinguish between the two, and late-bound callers
213                 # do not even read the type library. Thus the type library is really
214                 # only a convience to early-bound callers so they can perform some
215                 # type checking at compile time.
216                 #
217                 # IMPORTANT NOTE:
218                 #
219                 # There appears to be no way to distinguish between the COM caller
220                 # sending a VT_I4 that happens to contain the value -2147352572 and
221                 # sending a VT_ERROR with the error code set to
222                 # DISP_E_PARAMNOTFOUND. Thus, if the argument's metadata declares it
223                 # to be a IntegerTypeMetadata, we assume the caller passed the VT_I4, not
224                 # the VT_ERROR, but issue a warning about it. This means that
225                 # callers must explicitly send all integer arguments.
226
227                 if not memberMetadata.Arguments[len(memberMetadata.Arguments) - 1].IsArbitraryArgumentList:      # No [in, optional] or [in, defaultvalue] args are allowed if varargs parameter is present
228                     for i in range(0, len(args)):
229                         if args[i] == winerror.DISP_E_PARAMNOTFOUND and i < len(memberMetadata.Arguments) - 1:
230                             argMetadata = memberMetadata.Arguments[i + 1]
231                             if argMetadata.IsFormalParameter and argMetadata.HasDefault:
232                                 if not isinstance(argMetadata.Type, IntegerTypeMetadata):
233                                     args[i] = argMetadata.Default
234                                     argsChanged = True
235                                 else:
236                                     logger = logging.getLogger(u'GeoEco.COM')
237                                     if logger is not None:
238                                         logger.warning(_(u'COM object 0x%08X: Method %s.%s received the value %i for the %s parameter. Due to the design of the pythoncom Python library, this method cannot determine if the caller provided an integer with this value or a VARIANT of type VT_ERROR with error code DISP_E_PARAMNOTFOUND, indicating that the caller is not providing a value. (From .Net code, this is done by passing System.Type.Missing.) This method will assume that the caller provided the integer, not the VT_ERROR VARIANT. If this was not your intention, and you wanted the method to use its default value for this parameter, you should pass in the default value %i explicitly.'), id(self._obj_), argMetadata.Method.Name, argMetadata.Method.Class.Name, winerror.DISP_E_PARAMNOTFOUND, argMetadata.Name, argMetadata.Default)
239
240                 # If the last argument of this method is an arbitrary argument list -
241                 # [vararg] in IDL - then we must handle a quirk (bug?) in pythoncom
242                 # that relates to how the method is invoked. According to the COM
243                 # rules, methods that have a [vararg] must pass the remaining args
244                 # in a SAFEARRAY, and it is the responsibility of the called method
245                 # to unpack the SAFEARRAY. When we're invoked through the IDispatch
246                 # mechanism, the pythoncom code seems to do this for us, and the
247                 # additional args appear as additional elements in the "args"
248                 # parameter of _invokeex_. But when we're invoked through normal
249                 # COM (a "vtable" call), the pythoncom code that is used in that
250                 # situation (the "universal dispatcher"?) does not seem to unpack
251                 # the array for us: the additional args appear in a single tuple
252                 # appended to the "args" parameter of _invokeex_.
253                 #
254                 # So, if the last argument of this method is a [vararg], inspect the
255                 # stack to determine whether we were invoked through the "universal
256                 # dispatcher". (The only way I know how to tell is to inspect the
257                 # stack.) If so, unpack the tuple.
258
259                 if memberMetadata.Arguments[len(memberMetadata.Arguments) - 1].IsArbitraryArgumentList and len(args) == len(memberMetadata.Arguments) - 1:
260                     if isinstance(args[len(args) - 1], types.TupleType) and self._InvokedByUniversalDispatcher():
261                         varargTuple = args.pop()
262                         args = args + list(varargTuple)
263                         argsChanged = True
264
265             # Convert the arguments list back to a tuple.
266             
267             args = tuple(args)
268
269             if argsChanged:           
270                 self._Log(_(u'COM object 0x%08X: Fixed arguments provided by pythoncom. New args = %s'), id(self._obj_), repr(args))
271
272             # Use the base class to invoke the member and capture its result. If
273             # we were unable to look up the method name, the base class will
274             # raise a COMException.
275
276             returnedValue = win32com.server.policy.DesignatedWrapPolicy._invokeex_(self, dispid, lcid, wFlags, args, kwArgs, serviceProvider)
277
278             # If the member is either a property that is a Python tuple or a
279             # method that returns a one result that is a tuple, coerce this
280             # tuple into a list before returning it. This will prevent pythoncom
281             # from trying to unpack multiple return values from it. (Pythoncom
282             # treats all tuples as multiple return values, which is reasonable
283             # given that it has no access to our metadata. If it receives a
284             # tuple of four values, it cannot tell if it is four separate return
285             # values or one array value with four elements.)
286
287             if isinstance(returnedValue, types.TupleType) and (isinstance(memberMetadata, PropertyMetadata) and isinstance(memberMetadata.Type, TupleTypeMetadata) or isinstance(memberMetadata, MethodMetadata) and len(memberMetadata.Results) == 1 and isinstance(memberMetadata.Results[0].Type, TupleTypeMetadata)):
288                 returnedValue = list(returnedValue)
289                 self._Log(_(u'COM object 0x%08X: Coerced the returned Python tuple into a Python list, so pythoncom will return it as a SAFEARRAY'), id(self._obj_))
290
291             # If the member is a method that returns multiple results and we
292             # were invoked through normal COM (a "vtable" call, which comes
293             # through pythoncom's "universal dispatcher"), pythoncom is
294             # expecting the first value of the returned tuple to be an HRESULT.
295             # This is a behavior difference in pythoncom: it does not expect the
296             # HRESULT to be prepended to the output tuple when the method is
297             # invoked through the IDispatch mechanism. I noticed an email
298             # exchange in the pywin32 forum in late 2006 indicating that Mark
299             # Hammond is now aware of this problem. For now, we have to prepend
300             # the HRESULT S_OK (0) when invoked through the universal
301             # dispatcher, or it will fail with an error similar to this:
302             #
303             #     exceptions.TypeError: Expected 5 return values, got: 4
304             #
305             # (In this example, the method returned four results. Pythoncom was
306             # looking for a fifth, the HRESULT.)
307
308             if isinstance(memberMetadata, MethodMetadata) and len(memberMetadata.Results) > 1 and isinstance(returnedValue, types.TupleType) and len(returnedValue) == len(memberMetadata.Results) and self._InvokedByUniversalDispatcher():
309                 returnedValue = tuple([winerror.S_OK] + list(returnedValue))
310                 self._Log(_(u'COM object 0x%08X: Prepended the HRESULT S_OK to the tuple of output arguments, so pythoncom\'s "universal dispatcher" will not fail.'), id(self._obj_))
311                 # NOTE: IT STILL FAILS, JUST IN A DIFFERENT PLACE. I DO NOT KNOW WHAT TO DO...
312
313             # Return the result.
314             
315             self._Log(_(u'COM object 0x%08X: InvokeEx returning %s'), id(self._obj_), repr(returnedValue))
316             return returnedValue           
317
318         def _Log(self, format, *args):           
319             logger = logging.getLogger(u'GeoEco.COM')
320             if logger is not None:
321                 logger.debug(format, *args)
322
323         def _TuplesToLists(self, arg, typeMetadata, depthToDescend=None):
324
325             # It may be that we have several levels of nesting. If the developer
326             # cooked up some esoteric data structure such as a tuple of tuples
327             # of lists, we need to convert only the most-interior sequences to
328             # lists. But the outer tuples are immutable, so to accomplish this,
329             # we must convert them to lists, change the inner-most tuples to
330             # lists, convert the outer ones back to tuples. To accomplish this
331             # most efficiently, we determine ahead of time how deeply we need
332             # to descend into the nested sequences.
333
334             if depthToDescend is None:
335                 currentDepth = 0
336                 depthToDescend = 0
337                 t = typeMetadata
338                 while isinstance(t, SequenceTypeMetadata):
339                     currentDepth += 1
340                     t = t.ElementType
341                     if isinstance(t, ListTypeMetadata):
342                         depthToDescend = currentDepth
343
344             # If the arg at the current level should be a list, convert it now.                       
345
346             if isinstance(arg, types.TupleType) and isinstance(typeMetadata, ListTypeMetadata):
347                 arg = list(arg)
348
349             # If a deeper level needs conversion, convert the current level to a
350             # list, do the child levels, and convert the current level back if
351             # necessary.
352
353             if depthToDescend > 0:
354                 arg = list(arg)
355                 for i in range(len(arg)):
356                     arg[i] = self._TuplesToLists(arg[i], typeMetadata.ElementType, depthToDescend - 1)
357                 if isinstance(typeMetadata, TupleTypeMetadata):
358                     arg = tuple(arg)
359
360             # Return
361             
362             return arg
363
364         def _InvokedByUniversalDispatcher(self):
365             currentFrame = inspect.currentframe()
366             try:
367                 outerFrames = inspect.getouterframes(currentFrame)
368                 try:
369                     for frame in outerFrames:
370                         if frame[1].endswith(u'win32com\\universal.py'):
371                             return True
372                 finally:
373                     del outerFrames
374             finally:
375                 del currentFrame
376             return False
377
378
379     def ValidateClassMetadata(cls):
380         assert inspect.isclass(cls), u'cls must be a class'
381         assert isinstance(cls.__doc__, DynamicDocString) and isinstance(cls.__doc__.Obj, ClassMetadata), u'cls.__doc__ must be an instance of GeoEco.Metadata.DynamicDocString and cls.__doc__.Obj must be an instance of GeoEco.Metadata.ClassMetadata.'
382
383         # Validate the class-level metadata.
384
385         classMetadata = cls.__doc__.Obj
386        
387         assert classMetadata.IsExposedAsCOMServer, u'cls.__doc__.Obj.IsExposedAsCOMServer must be True'
388         assert classMetadata.COMIID is not None, u'Class %s.%s is flagged for exposure by COM but its COMIID property is None. You must specify a value for COMIID; here is a randomly generated value you can use: %s. See the documentation for GeoEco.Metadata.ClassMetadata.COMIID for more information.' % (classMetadata.Module.Name, classMetadata.Name, repr(unicode(pythoncom.CreateGuid())))
389         assert classMetadata.COMCLSID is not None, u'Class %s.%s is flagged for exposure by COM but its COMCLSID property is None. You must specify a value for COMCLSID; here is a randomly generated value you can use: %s. See the documentation for GeoEco.Metadata.ClassMetadata.COMCLSID for more information.' % (classMetadata.Module.Name, classMetadata.Name, repr(unicode(pythoncom.CreateGuid())))
390         assert classMetadata.COMIID != classMetadata.COMCLSID, u'Class %s.%s is flagged for exposure by COM but its COMCLSID property has the same value as is COMIID property. These two values must not be the same. Please change one to a unique GUID; here is a randomly generated value you can use: %s. See the documentation for GeoEco.Metadata.ClassMetadata.COMCLSID for more information.' % (classMetadata.Module.Name, classMetadata.Name, repr(unicode(pythoncom.CreateGuid())))
391
392         # Validate the properties' metadata.
393
394         atLeastOneExposed = False
395
396         for propName, prop in inspect.getmembers(cls, inspect.isdatadescriptor):
397             if hasattr(prop, u'__doc__') and isinstance(prop.__doc__, DynamicDocString) and isinstance(prop.__doc__.Obj, PropertyMetadata) and prop.__doc__.Obj.IsExposedByCOM:
398                 atLeastOneExposed = True
399                 propMetadata = prop.__doc__.Obj
400                 assert propMetadata.Type.CanBeCOMParameter, u'Property %s.%s.%s is flagged for exposure by COM but %s.%s.%s.Type.CanBeCOMParameter is False. Change the property\'s type to one that can be exposed as a COM parameter or do not designate this property for exposure by COM.' % (propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name, propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name)
401                 assert propMetadata.Type.COMIDLType is not None, u'Property %s.%s.%s is flagged for exposure by COM but %s.%s.%s.Type.COMIDLType is None. This should not be possible, since the type\'s CanBeCOMParameter property is True. Please fix the definition of this type to include a COMIDLType or to assign CanBeCOMParameter to False.' % (propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name, propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name)
402                 assert not propMetadata.Type.CanBeNone or propMetadata.Type.COMIDLType == 'VARIANT', u'Property %s.%s.%s is flagged for exposure by COM but its Type.CanBeNone == True while its Type.COMIDLType != \'VARIANT\'. This is not allowed; if Type.CanBeNone is True then Type.COMIDLType must be \'VARIANT\', so that COM callers can pass a VT_NULL or VT_EMPTY. Please fix this.' % (propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name)
403                 assert prop.fget is not None, u'Property %s.%s.%s is flagged for exposure by COM but it has no "fget" method. This means it is a write-only property, which is not presently supported by GeoEco. Please give this property an fget method or do not flag it for exposure using COM.' % (propMetadata.Class.Module.Name, propMetadata.Class.Name, propMetadata.Name)
404
405         # Validate the methods' metadata.       
406
407         for methodName, method in inspect.getmembers(cls, inspect.ismethod):
408
409             # If this is the __init__ method, make sure all of its arguments
410             # have defaults. pythoncom cannot instantiate the class if any
411             # __init__ arguments require values.
412
413             (args, varargs, varkw, defaults) = inspect.getargspec(method)
414
415             if methodName == u'__init__':
416                 assert len(args) >= 1, u'Method %s.%s.%s must have at least one argument ("self") because it is the class\'s constructor.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name)
417                 assert len(args) <= 1 or isinstance(defaults, types.TupleType) and len(defaults) == len(args) - 1, u'Class %s.%s is flagged for exposure by COM but not all of the arguments to its __init__ have default values. All __init__ arguments must have default values for pythoncom to be able to instantiate the class. Please provide defaults for every argument (except the "self" argument) or do not expose the class using COM.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name)
418
419             # If this method is flagged for exposure by COM, validate the
420             # metadata for its arguments and results.
421             
422             if hasattr(method, u'__doc__') and isinstance(method.__doc__, DynamicDocString) and isinstance(method.__doc__.Obj, MethodMetadata) and method.__doc__.Obj.IsExposedByCOM:
423                 atLeastOneExposed = True
424                 methodMetadata = method.__doc__.Obj
425
426                 # Validate the arguments' metadata.
427
428                 assert varkw is None, u'Method %s.%s.%s is flagged for exposure by COM but it has an optional keywords dictionary argument (**%s). This is not supported by COM. Please remove this argument or do not expose this method by COM.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, varkw)
429                 expectedArgCount = len(args)
430                 if varargs is not None:
431                     expectedArgCount += 1
432                 assert len(methodMetadata.Arguments) == expectedArgCount, u'Method %s.%s.%s is flagged for exposure by COM but its metadata is not consistent with its definition: the metadata lists a different number of arguments than the method definition. Please correct the metadata.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name)
433
434                 foundArgWithDefault = False
435                
436                 for i in range(len(methodMetadata.Arguments)):
437                     argMetadata = methodMetadata.Arguments[i]
438                     if i < len(args):
439                         assert args[i] == argMetadata.Name, u'Method %s.%s.%s is flagged for exposure by COM but its metadata is not consistent with its definition: Argument %i is named %s in the metadata but %s in the definition. Please correct the metadata.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, i, argMetadata.Name, args[i])
440                     else:
441                         assert varargs == argMetadata.Name, u'Method %s.%s.%s is flagged for exposure by COM but its metadata is not consistent with its definition: Argument %i is named %s in the metadata but %s in the definition. Please correct the metadata.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, i, argMetadata.Name, varargs)
442                     if i > 0:
443                         assert argMetadata.Type.CanBeCOMParameter, u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" argument has Type.CanBeCOMParameter == False. Change the arguments\'s type to one that can be exposed as a COM parameter or do not designate this method for exposure by COM.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, argMetadata.Name)
444                         assert argMetadata.Type.COMIDLType is not None, u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" argument has Type.COMIDLType == None. This should not be possible, since the type\'s CanBeCOMParameter property is True. Please fix the definition of this type to include a COMIDLType or to assign CanBeCOMParameter to False.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, argMetadata.Name)
445                         assert not argMetadata.Type.CanBeNone or argMetadata.Type.COMIDLType == 'VARIANT', u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" argument has Type.CanBeNone == True but Type.COMIDLType != \'VARIANT\'. This is not allowed; if Type.CanBeNone is True then Type.COMIDLType must be \'VARIANT\', so that COM callers can pass a VT_NULL or VT_EMPTY. Please fix this.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, argMetadata.Name)
446
447                 # Validate the results' metadata.
448                 
449                 for resultMetadata in methodMetadata.Results:
450                     assert resultMetadata.Type.CanBeCOMParameter, u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" result has Type.CanBeCOMParameter == False. Change the result\'s type to one that can be exposed as a COM parameter or do not designate this method for exposure by COM.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, resultMetadata.Name)
451                     assert resultMetadata.Type.COMIDLType is not None, u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" result has Type.COMIDLType == None. This should not be possible, since the type\'s CanBeCOMParameter property is True. Please fix the definition of this type to include a COMIDLType or to assign CanBeCOMParameter to False.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, resultMetadata.Name)
452                     assert not resultMetadata.Type.CanBeNone or resultMetadata.Type.COMIDLType == 'VARIANT', u'Method %s.%s.%s is flagged for exposure by COM but the metadata for its "%s" result has Type.CanBeNone == True but Type.COMIDLType != \'VARIANT\'. This is not allowed; if Type.CanBeNone is True then Type.COMIDLType must be \'VARIANT\', so that a VT_NULL VARIANT may be returned to the COM when the Python method returns None. Please fix this.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name, resultMetadata.Name)
453
454         # Validate that the caller is not trying to expose static methods using
455         # COM.
456
457         for funcName, func in inspect.getmembers(cls, inspect.isfunction):
458             if hasattr(func, u'__doc__') and isinstance(func.__doc__, DynamicDocString) and isinstance(func.__doc__.Obj, MethodMetadata) and func.__doc__.Obj.IsExposedByCOM:
459                 methodMetadata = func.__doc__.Obj
460                 assert not methodMetadata.IsStaticMethod, u'Static method %s.%s.%s is flagged for exposure by COM. This is not supported. Please change this method to a class method if you want to expose it using COM.' % (methodMetadata.Class.Module.Name, methodMetadata.Class.Name, methodMetadata.Name)
461
462         # Validate that at least one property or method is flagged for exposure
463         # by COM.
464
465         assert atLeastOneExposed, u'Class %s.%s is flagged for exposure by COM but it does not have any properties or methods flagged for exposure by COM. At least one property or method must be flagged for exposure, or you should not flag the class for exposure.' % (classMetadata.Module.Name, classMetadata.Name)
466
467
468     def GetIDLInterfaceDefinitionFromMetadata(classMetadata):
469         assert isinstance(classMetadata, ClassMetadata), u'classMetadata must be an instance of GeoEco.Metadata.ClassMetadata'
470
471         # Build the IDL header.
472         
473         header = '[\n    dual,\n    uuid(%s),\n    helpstring("I%s Interface"),\n    pointer_default(unique)\n]\ninterface I%s : IDispatch\n{\n' % (classMetadata.COMIID[1:-1], classMetadata.Name, classMetadata.Name)
474
475         # Build a list of IDL property definitions.
476
477         dispID = 1
478         propertiesIDL = []
479
480         properties = inspect.getmembers(classMetadata.Object, inspect.isdatadescriptor)
481         for (name, prop) in properties:
482             if isinstance(prop.__doc__, DynamicDocString) and isinstance(prop.__doc__.Obj, PropertyMetadata) and prop.__doc__.Obj.IsExposedByCOM:
483                 if len(propertiesIDL) <= 0:
484                     propertiesIDL = ['    // Properties\n']
485                 propertiesIDL.append(GetIDLPropertyDefinitionFromMetadata(prop.__doc__.Obj, dispID))
486                 dispID += 1
487
488         # Build a list of IDL method definitions.
489
490         methodsIDL = []
491
492         methods = inspect.getmembers(classMetadata.Object, inspect.ismethod)
493         for (name, method) in methods:
494             if isinstance(method.__doc__, DynamicDocString) and isinstance(method.__doc__.Obj, MethodMetadata) and method.__doc__.Obj.IsExposedByCOM:
495                 if len(methodsIDL) <= 0:
496                     methodsIDL = ['    // Methods\n']
497                 methodsIDL.append(GetIDLMethodDefinitionFromMetadata(method.__doc__.Obj, dispID))
498                 dispID += 1
499
500         # Build the IDL footer.
501         
502         footer = '};\n'
503
504         # Return the complete IDL inteface definition.
505
506         return header + ''.join(propertiesIDL) + ''.join(methodsIDL) + footer
507
508
509     def GetIDLPropertyDefinitionFromMetadata(propertyMetadata, dispID):
510         assert isinstance(propertyMetadata, PropertyMetadata), u'propertyMetadata must be an instance of GeoEco.Metadata.PropertyMetadata'
511         assert isinstance(dispID, types.IntType), u'dispID must be an int'
512
513         # If the IDL for the property's type is a SAFEARRAY, use a VARIANT
514         # instead to work-around a pythoncom bug: when early-bound callers
515         # invoke a method that a returns a SAFEARRAY, pythoncom raises a
516         # TypeError when the method returns, similar to this:
517         #
518         #     exceptions.TypeError: The VARIANT type is unknown (0x600c).
519         #
520         # The error does not occur when we specify a VARIANT in the IDL rather
521         # than a SAFEARRAY. The unfortunate side effect is that early-bound
522         # callers lose the benefits of type checking.
523         #
524         # Note that we cannot use a SAFEARRAY for the propput function and a
525         # VARIANT for the propget function. This causes the .Net compiler to
526         # become confused and issue an error.
527
528         typeIDL = propertyMetadata.Type.COMIDLType
529         if typeIDL.upper().startswith('SAFEARRAY'):       
530             typeIDL = 'VARIANT'
531
532         # Generate a propget entry.
533
534         idl = '    [id(%s), propget, helpstring("%s Property")] HRESULT %s([out, retval] %s *pValue);\n' % (dispID, propertyMetadata.Name, propertyMetadata.Name, typeIDL)
535
536         # If the property is not read-only, generate a propput entry.
537
538         if not propertyMetadata.IsReadOnly:
539             idl = idl + '    [id(%s), propput, helpstring("%s Property")] HRESULT %s([in] %s value);\n' % (dispID, propertyMetadata.Name, propertyMetadata.Name, typeIDL)
540
541         return idl
542
543
544     def GetIDLMethodDefinitionFromMetadata(methodMetadata, dispID):
545         assert isinstance(methodMetadata, MethodMetadata), u'methodMetadata must be an instance of GeoEco.Metadata.MethodMetadata'
546         assert isinstance(dispID, types.IntType), u'dispID must be an int'
547
548         # Generate IDL for the method's arguments. The correct order is:
549         #
550         # 1. Required input arguments (those without a default)
551         # 2. Optional input arguments (those with a default)
552         # 3. 2nd and subsequent results (if any) as IDL [in, out] parameters
553         # 4. The varargs input argument (if any)
554         # 5. The 1st result (if any) as an IDL [out, retval] parameter
555         #
556         # The ValidateClassMetadata function, which should have been called
557         # prior to invoking this one, validates the correct order.
558
559         (args, varargs, varkw, defaults) = inspect.getargspec(methodMetadata.Object)
560
561         requiredInputParams = []
562         optionalInputParams = []
563         inOutParams = []
564         varargParam = []
565         retvalParam = []
566
567         foundOptionalVariant = False
568         foundDefaultvalue = False
569         varargAttribute = ''
570
571         # Loop through the input arguments, populating the lists defined above.       
572
573         for i in range(1, len(methodMetadata.Arguments)):
574             argMetadata = methodMetadata.Arguments[i]
575
576             # If the argument is a formal parameter (i.e. not varargs)...
577
578             if argMetadata.IsFormalParameter:
579                 assert len(varargParam) == 0, u'Programming error in GeoEco.COM: got formal parameter but varargParam is not empty'         # Python itself should prevent this from ever happening
580
581                 # If the argument has no default, then it is a required input
582                 # parameter. Also, if the method specifies a varargs parameter,
583                 # then ALL arguments are required, according to COM rules.
584
585                 if not argMetadata.HasDefault or varargs is not None:
586                     assert len(optionalInputParams) == 0, u'Programming error in GeoEco.COM: got required input argument but optionalInputParams is not empty'      # Python itself should prevent this from ever happening
587                     requiredInputParams.append('[in] %s %s' % (argMetadata.Type.COMIDLType, argMetadata.Name))
588
589                 # The argument has a default. Try to produce an IDL
590                 # representation of it. (Python allows specification of very
591                 # sophisticated default values; IDL does not.)
592
593                 else:
594                     if argMetadata.Default is not None:
595                         idlForDefault = argMetadata.Type.COMIDLRepresentationOfValue(argMetadata.Default)
596                     else:
597                         idlForDefault = None
598                     assert isinstance(idlForDefault, (types.StringType, types.NoneType)), u'Programming error in %s: COMIDLRepresentationOfValue must return an 8-bit string or None.' % argMetadata.Type.__class__.__name__
599
600                     # If we failed to produce an IDL representation for the
601                     # parameter, we would like to promote this parameter from
602                     # optional to required, because the caller will have to
603                     # specify it (because we can't tell IDL the default value).
604                     # Specify it as required so long as it can't be None (in
605                     # which case it should be [in, optional]) and we haven't
606                     # processed any previous optional parameters.
607
608                     if idlForDefault is None and not argMetadata.Type.CanBeNone and len(optionalInputParams) <= 0:
609                         requiredInputParams.append('[in] %s %s' % (argMetadata.Type.COMIDLType, argMetadata.Name))
610
611                     # Either the argument's default can be represented in IDL, or
612                     # it can be None, or we have already processed a previous
613                     # optional parameter. We want to specify this argument as
614                     # [in, defaultvalue] but may be forced to specify it as
615                     # [in, optional] due to some IDL esoterics.
616                     #
617                     # As above, if the argument's Type.CanBeNone is True, we
618                     # MUST use VARIANT as the IDL parameter type. But when we
619                     # do, the MIDL compiler does not let us specify
620                     # [in, defaultvalue] for the parameter, we can only specify
621                     # [in, optional].
622                     #
623                     # On the other hand, if the argument's Type.CanBeNone is
624                     # False, we can use the argument's Type.COMIDLType and
625                     # specify [in, defaultvalue], but ONLY IF we have not
626                     # specified [in, optional] for any previous parameters. Once
627                     # we have specified one [in, optional], all following [in]
628                     # parameters must be [in, optional] (unless they are
629                     # [in, out] which is convered later, when the IDL for
630                     # methodMetadata.Results is generated).
631                     #
632                     # Finally if we the argument's default cannot be represented
633                     # in IDL, we have no choice but to use [in, optional], since
634                     # we don't know how to specify [defaultvalue].
635                     #
636                     # This situation is quite complicated and goes against older COM
637                     # documentation, which showed that you could specify
638                     # [in, optional, defaultvalue] parameters. Now [optional] and
639                     # [defaultvalue] are mutually exclusive. The design chosen here
640                     # is a compromise: so long as the method developer puts all
641                     # optional, CanBeNone arguments at the end of the method
642                     # definition, strong IDL types will be used for the preceding
643                     # optional, non-CanBeNone arguments.
644
645                     else:
646                         if idlForDefault is None or argMetadata.Type.CanBeNone or foundOptionalVariant:
647                             optionalInputParams.append('[in, optional] VARIANT %s' % argMetadata.Name)
648                             foundOptionalVariant = True
649                         else:
650                             requiredInputParams.append('[in, defaultvalue(%s)] %s %s' % (idlForDefault, argMetadata.Type.COMIDLType, argMetadata.Name))
651                             foundDefaultvalue = True
652
653             # If the argument is the arbitrary argument list (the "varargs"
654             # parameter) we have to specify it in IDL as
655             # [in] SAFEARRAY(VARIANT).
656             
657             elif argMetadata.IsArbitraryArgumentList:
658                 assert len(varargParam) == 0, u'Programming error in GeoEco.COM: got vararg but varargParam is not empty'       # Python itself should prevent this from ever happening
659                 varargParam.append('[in] SAFEARRAY(VARIANT) %s' % argMetadata.Name)
660                 varargAttribute = ', vararg'
661
662             # The argument must be the keyword argument dictionary ("kwargs").
663             # We do not support exposing methods with this kind of argument
664             # through COM because COM has no support for it. Raise an error.
665             # Note that this is a programming error in GeoEco.COM because we
666             # should have detected this problem previously when we validated the
667             # method in ValidateClassMetadata.
668
669             else:
670                 assert False, u'Programming error in GeoEco.COM: argMetadata.IsArbitraryArgumentList is True'
671
672         # Loop through the results, populating the lists defined above.       
673
674         for i in range(len(methodMetadata.Results)):
675             resultMetadata = methodMetadata.Results[i]
676             idlType = resultMetadata.Type.COMIDLType
677
678             # If the IDL for the return type is a SAFEARRAY, use a VARIANT
679             # instead to work-around a pythoncom bug: when early-bound
680             # callers invoke a method that a returns a SAFEARRAY, pythoncom
681             # raises a TypeError when the method returns, similar to this:
682             #
683             #    exceptions.TypeError: The VARIANT type is unknown (0x600c).
684             #
685             # The error does not occur when we specify a VARIANT in the IDL
686             # rather than a SAFEARRAY. The unfortunate side effect is that
687             # early-bound callers lose the benefits of type checking.
688
689             if idlType.upper().startswith('SAFEARRAY'):
690                 idlType = 'VARIANT'
691
692             # If this is the first result, it will be the IDL [out, retval]
693             # parameter, which is the actual return value of the method.
694             # Otherwise it is an [in, out] parameter that is overwritten by the
695             # method.
696
697             if i == 0:
698                 retvalParam.append('[out, retval] %s *%s' % (idlType, resultMetadata.Name))
699
700             # Otherwise, if the input parameters did not include any
701             # [in, optional] or [in, defaultvalue] parameters, this result
702             # should be an [in, out] parameter.
703             
704             elif not foundOptionalVariant and not foundDefaultvalue:
705                 inOutParams.append('[in, out] %s *%s' % (idlType, resultMetadata.Name))
706
707             # Otherwise this result must be an [in, out, optional] VARIANT * or
708             # the MIDL compiler will fail.
709
710             else:
711                 inOutParams.append('[in, out, optional] VARIANT *%s' % (resultMetadata.Name))
712
713         # Return the full IDL method signature.
714
715         idl = '    [id(%s), helpstring("%s Method")%s] HRESULT %s(%s);\n' % (dispID, methodMetadata.Name, varargAttribute, methodMetadata.Name, ', '.join(requiredInputParams + optionalInputParams + inOutParams + varargParam + retvalParam))
716         return idl       
717
718
719     def RegisterCOMServerUsingClassMetadata(moduleName, className):
720
721         # Import the module and obtain the class's metadata.
722
723         assert isinstance(moduleName, basestring), u'moduleName must be a string.'
724         assert isinstance(className, basestring), u'className must be a string.'
725         __import__(moduleName)
726         classMetadata = sys.modules[moduleName].__dict__[className].__doc__.Obj
727
728         # Register the class.
729
730         import win32com.server.register
731         try:
732             win32com.server.register.RegisterServer(clsid=classMetadata.COMCLSID,
733                                                     pythonInstString=moduleName + '.' + className,
734                                                     desc=classMetadata.ShortDescription,
735                                                     progID=classMetadata.COMVersionIndependentProgID,
736                                                     verProgID=classMetadata.COMVersionDependentProgID,
737                                                     policy=__name__ + '.' + MetadataDesignatedWrapPolicy.__name__,
738                                                     dispatcher=__name__ + '.' + ExceptionTranslatingDispatcher.__name__)
739         except Exception, e:
740             return 'The Python win32com.server.register.RegisterServer function raised %s: %s' % (e.__class__.__name__, str(e))
741
742         return None
743
744
745     def UnregisterCOMServerUsingClassMetadata(moduleName, className):
746
747         # Import the module and obtain the class's metadata.
748
749         assert isinstance(moduleName, basestring), u'moduleName must be a string.'
750         assert isinstance(className, basestring), u'className must be a string.'
751         __import__(moduleName)
752         classMetadata = sys.modules[moduleName].__dict__[className].__doc__.Obj
753
754         # Unregister the class.
755
756         import win32com.server.register
757         try:
758             win32com.server.register.UnregisterServer(clsid=classMetadata.COMCLSID,
759                                                       progID=classMetadata.COMVersionIndependentProgID,
760                                                       verProgID=classMetadata.COMVersionDependentProgID)
761         except:
762             pass
763
764
765     class COMAutomationObject(object):
766
767         def __init__(self, progID, obj=None):
768             assert isinstance(progID, basestring), u'progID must be a string.'
769             assert isinstance(obj, (win32com.client.CDispatch, types.NoneType)), u'obj must be an instance of win32com.client.CDispatch, or None.'
770
771             self._ProgID = unicode(progID)
772             self._WrappedMethods = {}
773
774             if obj is not None:
775                 self._Object = obj
776             else:
777                 try:
778                     self._Object = win32com.client.Dispatch(progID)
779                 except pythoncom.com_error, (hr, msg, exc, arg):
780                     from GeoEco.Logging import Logger
781                     Logger.RaiseException(RuntimeError(_(u'Failed to instantiate the %(progid)s COM Automation object. win32com.client.Dispatch reported %(error)s') % {u'progid': progID, u'error': FormatCOMError(hr, msg, exc, arg)}))
782                 except Exception, e:
783                     from GeoEco.Logging import Logger
784                     Logger.RaiseException(RuntimeError(_(u'Failed to instantiate the %(progid)s COM Automation object. win32com.client.Dispatch reported %(error)s: %(msg)s') % {u'progid': progID, u'error': e.__class__.__name__, u'msg': unicode(e)}))
785                
786                 self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Instantiated.') % {u'progid': self._ProgID, u'id': id(self._Object)})
787
788         def __del__(self):           
789             self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Last reference released.') % {u'progid': self._ProgID, u'id': id(self._Object)})
790
791         def _LogDebug(self, format, *args, **kwargs):
792             try:
793                 logging.getLogger(u'GeoEco.COM').debug(format, *args, **kwargs)
794             except:
795                 pass
796
797         def _LogInfo(self, format, *args, **kwargs):
798             try:
799                 logging.getLogger(u'GeoEco.COM').info(format, *args, **kwargs)
800             except:
801                 pass
802
803         def _LogWarning(self, format, *args, **kwargs):
804             try:
805                 logging.getLogger(u'GeoEco.COM').warning(format, *args, **kwargs)
806             except:
807                 pass
808
809         def _LogError(self, format, *args, **kwargs):
810             try:
811                 logging.getLogger(u'GeoEco.COM').error(format, *args, **kwargs)
812             except:
813                 pass
814
815         def __getattr__(self, name):
816             assert isinstance(name, basestring), u'name must be a string.'
817
818             # If the caller is asking for a private attribute (the name starts with
819             # an underscore), he wants an attribute of the wrapper class instance,
820             # not of the wrapped object. In this case, we must use the object
821             # class's implementation of __getattr__.
822
823             if isinstance(name, types.StringType):
824                 name = unicode(name)
825             if name.startswith(u'_'):
826                 return object.__getattribute__(self, name)
827
828             # The caller is asking for a data attribute or a method of the wrapped
829             # object. If we already built a wrapper method for the specified name,
830             # it means the caller asked for it before and we determined that it was
831             # a method. Return the wrapped method to the caller now.
832
833             if self._WrappedMethods.has_key(name.lower()):
834                 return new.instancemethod(self._WrappedMethods[name.lower()], self, COMAutomationObject)
835
836             # Retrieve the attribute from the wrapped object.
837
838             try:
839                 value = getattr(self._Object, name)
840             except pythoncom.com_error, (hr, msg, exc, arg):
841                 from GeoEco.Logging import Logger
842                 Logger.RaiseException(AttributeError(_(u'Failed to get the %(attr)s property or method of the %(progid)s COM Automation object 0x%(id)08X. win32com.client.Dispatch reported %(error)s') % {u'attr': name, u'progid': self._ProgID, u'id': id(self._Object), u'error': FormatCOMError(hr, msg, exc, arg)}))
843             except Exception, e:
844                 from GeoEco.Logging import Logger
845                 Logger.RaiseException(AttributeError(_(u'Failed to get the %(attr)s property or method of the %(progid)s COM Automation object 0x%(id)08X. win32com.client.Dispatch reported %(error)s: %(msg)s') % {u'attr': name, u'progid': self._ProgID, u'id': id(self._Object), u'error': e.__class__.__name__, u'msg': unicode(e)}))
846
847             # If it is a method, create a wrapper, add it to our dictionary of
848             # wrapped methods, and return the wrapper.
849
850             if isinstance(value, (types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType)):
851
852                 # Write the methods's definition. If the method has VT_MISSING
853                 # or VT_EMPTY as default values, these are represented by
854                 # pythoncom.Missing and pythoncom.Empty. inspect.formatargspec
855                 # puts in the strings '<PyOleMissing object at 0x0148DFF8>' and
856                 # '<PyOleEmpty object at 0x01481C40>' (with different hex
857                 # addresses). These strings will not compile, so we must replace
858                 # them with pythoncom.Missing and pythoncom.Empty.
859
860                 (args, varargs, varkw, defaults) = inspect.getargspec(value)
861                 sourceCode = 'def %s%s:\n' % (str(name), inspect.formatargspec(args, varargs, varkw, defaults))
862                 sourceCode = re.subn('<PyOleMissing object at 0x........>', 'pythoncom.Missing', sourceCode)[0]
863                 sourceCode = re.subn('<PyOleEmpty object at 0x........>', 'pythoncom.Empty', sourceCode)[0]
864
865                 # Write the method's body.
866                 
867                 sourceCode = sourceCode + '    return self._InvokeMethod(self._Object.%s)\n' % name
868
869                 # Compile the method.
870                 
871                 _locals = {}
872                 exec sourceCode in globals(), _locals
873
874                 # Add it to our dictionary of wrapped methods.
875                 
876                 self._WrappedMethods[name.lower()] = _locals[name]
877
878                 # Return it to the caller.
879                 
880                 return new.instancemethod(_locals[name], self, COMAutomationObject)
881
882             # Log the returned value.
883
884             self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Get %(name)s returned %(value)s') % {u'progid': self._ProgID, u'id': id(self._Object), u'name': name, u'value': repr(value)})
885
886             # The returned value is a property. Convert it from pythoncom's
887             # preferred type to the type we prefer.
888
889             value = self._FromPythonCOMType(value)
890
891             # Return the value.
892
893             return value
894
895         def __setattr__(self, name, value):
896             assert isinstance(name, basestring), u'name must be a string.'
897
898             # If the caller is asking for a private attribute (the name starts with
899             # an underscore), he wants to set an attribute of the wrapper class
900             # instance, not of the wrapped object. In this case, we must use the
901             # object class's implementation of __setattr__.
902
903             if isinstance(name, types.StringType):
904                 name = unicode(name)
905             if name.startswith(u'_'):
906                 return object.__setattr__(self, name, value)
907
908             # The caller wants to set a property of the wrapped object. First
909             # convert it from the type we prefer to the one perferred by
910             # pythoncom.
911
912             value = self._ToPythonCOMType(value)           
913
914             # Set the property.
915
916             try:
917                 setattr(self._Object, name, value)
918             except pythoncom.com_error, (hr, msg, exc, arg):
919                 from GeoEco.Logging import Logger
920                 Logger.RaiseException(AttributeError(_(u'Failed to set the %(attr)s property of the %(progid)s COM Automation object 0x%(id)08X to %(value)s. win32com.client.Dispatch reported %(error)s') % {u'attr': name, u'progid': self._ProgID, u'id': id(self._Object), u'value': repr(value), u'error': FormatCOMError(hr, msg, exc, arg)}))
921             except Exception, e:
922                 from GeoEco.Logging import Logger
923                 Logger.RaiseException(AttributeError(_(u'Failed to set the %(attr)s property of the %(progid)s COM Automation object 0x%(id)08X to %(value)s. win32com.client.Dispatch reported %(error)s: %(msg)s') % {u'attr': name, u'progid': self._ProgID, u'id': id(self._Object), u'value': repr(value), u'error': e.__class__.__name__, u'msg': unicode(e)}))
924
925             # Log the set value.
926
927             if isinstance(value, COMAutomationObject):
928                 self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Set %(name)s to %(progid2)s COM Automation object 0x%(id2)08X.') % {u'progid': self._ProgID, u'id': id(self._Object), u'name': name, u'progid2': value._ProgID, u'id2': id(value._Object)})
929             else:
930                 self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Set %(name)s to %(value)s') % {u'progid': self._ProgID, u'id': id(self._Object), u'name': name, u'value': repr(value)})
931
932         def _InvokeMethod(self, method):
933
934             # Convert the arguments from the types we prefer to those preferred
935             # by pythoncom.
936
937             (_args, _varargs, _varkw, _locals) = inspect.getargvalues(inspect.currentframe().f_back)
938             del _args[0]
939
940             for argName in _args:
941                 _locals[argName] = self._ToPythonCOMType(_locals[argName])
942
943             if _varargs is not None:
944                 _locals[_varargs] = self._ToPythonCOMType(_locals[_varargs])
945
946             # Build the source code needed to invoke the method.
947
948             sourceCode = 'self._Object.%s(' % method.__name__
949             for argName in _args:
950                 if not sourceCode.endswith('('):
951                     sourceCode = sourceCode + ', '
952                 sourceCode = sourceCode + '%s=_locals[\'%s\']' % (argName, argName)
953             if _varargs is not None:
954                 if not sourceCode.endswith('('):
955                     sourceCode = sourceCode + ', '
956                 sourceCode = sourceCode + '*_locals[\'%s\'] ' % _varargs
957             sourceCode = sourceCode + ')'
958
959             # Invoke the method.
960
961             self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: Invoking %(name)s%(args)s.') % {u'progid': self._ProgID, u'id': id(self._Object), u'name': method.__name__, u'args': inspect.formatargvalues(_args, _varargs, _varkw, _locals)})
962            
963             try:
964                 value = eval(sourceCode, globals(), locals())
965             except pythoncom.com_error, (hr, msg, exc, arg):
966                 from GeoEco.Logging import Logger
967                 Logger.RaiseException(RuntimeError(_(u'Execution of %(name)s%(args)s failed for the %(progid)s COM Automation object 0x%(id)08X. The function reported %(error)s') % {u'name': method.__name__, u'args': inspect.formatargvalues(_args, _varargs, _varkw, _locals), u'progid': self._ProgID, u'id': id(self._Object), u'error': FormatCOMError(hr, msg, exc, arg)}))
968             except Exception, e:
969                 from GeoEco.Logging import Logger
970                 Logger.RaiseException(RuntimeError(_(u'Execution of %(name)s%(args)s failed for the %(progid)s COM Automation object 0x%(id)08X. win32com.client.Dispatch reported %(error)s: %(msg)s') % {u'name': method.__name__, u'args': inspect.formatargvalues(_args, _varargs, _varkw, _locals), u'progid': self._ProgID, u'id': id(self._Object), u'error': e.__class__.__name__, u'msg': unicode(e)}))
971
972             # Log the returned value.
973
974             self._LogDebug(_(u'%(progid)s COM Automation object 0x%(id)08X: %(method)s returned %(value)s') % {u'progid': self._ProgID, u'id': id(self._Object), u'method': method.__name__, u'value': repr(value)})
975
976             # Convert the returned value from pythoncom's preferred type to the
977             # type we prefer.
978
979             value = self._FromPythonCOMType(value)
980                
981             # Return the returned value.
982             
983             return value
984
985         def _ToPythonCOMType(self, obj):
986             if isinstance(obj, types.ListType):
987                 return self._ToPythonCOMTypeList(obj)
988             if isinstance(obj, types.TupleType):
989                 l = list(obj)
990                 l = self._ToPythonCOMTypeList(l)
991                 return tuple(l)
992             if isinstance(obj, types.StringType):
993                 return UserPreferredEncodingToUnicode(obj)
994             if isinstance(obj, datetime.datetime):
995                 return pywintypes.Time((obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, 0, 0, 0))
996             if isinstance(obj, COMAutomationObject):
997                 return obj._Object
998             return obj
999
1000         def _ToPythonCOMTypeList(self, obj):
1001             for i in range(len(obj)):
1002                 obj[i] = self._ToPythonCOMType(obj[i])
1003             return obj
1004
1005         def _FromPythonCOMType(self, obj):
1006             if isinstance(obj, types.ListType):
1007                 return self._FromPythonCOMTypeList(obj)
1008             if isinstance(obj, types.TupleType):
1009                 l = list(obj)
1010                 l = self._FromPythonCOMTypeList(l)
1011                 return tuple(l)
1012             if isinstance(obj, pywintypes.TimeType):
1013                 return datetime.datetime(obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, obj.msec)
1014             if isinstance(obj, win32com.client.CDispatch):
1015                 return COMAutomationObject(u'<unnamed>', obj)
1016             return obj
1017
1018         def _FromPythonCOMTypeList(self, obj):
1019             for i in range(len(obj)):
1020                 obj[i] = self._FromPythonCOMType(obj[i])
1021             return obj
1022
1023
1024     def FormatCOMError(hr, msg, exc, arg):
1025
1026         # Adjust hr so that it will print out properly in hexadecimal.
1027         #
1028         # HRESULTs are 32-bit signed integers. In Intel architectures, like most
1029         # others, signed integers are stored in twos-complement form. In unsigned
1030         # hexadecimal, HRESULTS range from 0x80000000 to 087FFFFFF, which
1031         # corresponds to decimal numbers -2147483648 to -134217729.
1032         #
1033         # We want to print out the unsigned hexadecimal representation, since this
1034         # is what programmers are used to seeing, and what you can Google easily.
1035         # But Python 2.4.2's % formatter seems to be buggy:
1036         #
1037         #     >>> '%08X' % -2147483648      # We want '8000000'
1038         #     '-80000000'
1039         #     >>> '%08X' % -1               # We want 'FFFFFFFF'
1040         #     '-0000001'   
1041         #
1042         # But if we add 4294967296 (2^32), we get what we want:
1043         #
1044         #     >>> '%08X' % (-2147483648 + 4294967296)
1045         #     '80000000'
1046         #     >>> '%08X' % (-1 + 4294967296)
1047         #     'FFFFFFFF'
1048
1049         if hr < 0:
1050             hr = hr + 4294967296
1051
1052         # Format the message.
1053             
1054         s = u'COM Error 0x%08X' % hr
1055         if isinstance(msg, basestring):
1056             s = u'%s: "%s"' % (s, msg.strip())
1057         if not s.endswith(u'.'):
1058             s += u'.'
1059         if exc is not None:
1060             moreInfo = []
1061             (wcode, source, description, helpFile, helpId, scode) = exc
1062             if isinstance(source, basestring):
1063                 moreInfo.append(u'source="%s"' % source.strip())
1064             if isinstance(description, basestring):
1065                 moreInfo.append(u'description="%s"' % description.strip())
1066             if isinstance(scode, int) and scode != 0:
1067                 scodeMsg = None
1068                 try:
1069                     import win32api
1070                     scodeMsg = win32api.FormatMessage(scode)
1071                 except:
1072                     pass
1073                 if scode < 0:
1074                     scode += 4294967296
1075                 if scodeMsg is not None:
1076                     moreInfo.append(u'scode=0x%08X ("%s")' % (scode, scodeMsg))
1077                 else:
1078                     moreInfo.append(u'scode=0x%08X' % scode)
1079             if isinstance(wcode, int) and wcode != 0:
1080                 moreInfo.append(u'wcode=%i' % wcode)        # wcode is a 16-bit integer. I'm not sure how to best represent it.
1081             s += u' Error details: ' + u', '.join(moreInfo)
1082             if not s.endswith(u'.'):
1083                 s += u'.'
1084             if isinstance(helpFile, basestring) and isinstance(helpId, int):
1085                 s = _(u'%s More information may be found under topic %i of help file "%s".') % (s, helpId, helpFile)
1086
1087         return s
1088
1089
1090     ###############################################################################
1091     # Names exported by this module
1092     ###############################################################################
1093
1094     __all__ = ['TypeLibraryGUID',
1095                'TypeLibraryVersion',
1096                'TypeLibraryLCID',
1097                'ValidateClassMetadata',
1098                'GetIDLInterfaceDefinitionFromMetadata',
1099                'GetIDLPropertyDefinitionFromMetadata',
1100                'GetIDLMethodDefinitionFromMetadata',
1101                'RegisterCOMServerUsingClassMetadata',
1102                'UnregisterCOMServerUsingClassMetadata',
1103                'COMAutomationObject',
1104                'FormatCOMError']
1105
1106 else:
1107     __all__ = []
Note: See TracBrowser for help on using the browser.