So by default the Python Com Gateway class does not ship an intrinsic type library; this is a shame because Python has its own reflection capabilities and could do so easily IMHO. The official sample gives and compiles an Interface Definition Language (IDL) file but one has to maintain the IDL in sync with one's class. In this post I give code that gets Python to reflect on a class and automate the writing and compiling of the IDL into a type library.
Once you have a type library then you can create objects in VBA by adding a Tools->Reference and using New instead of using CreateObject(), you will get Intellisense . You will also get the object propertles in the VBA Locals and Watch windows.
DesignatedWrapPolicy
In Python COM behaviour is driven by policies, we want a type library so we'll need to use a DesignatedWrapPolicy policy which fortunately is the default. The doc string is worth quoting as it gives a round up of the attributes we need to give a class to make a type library appear,_typelib_guid_ and _typelib_version. This is taken from doc string of the DesignatedWrapPolicy class in win32com/server/policy.py . The opening remarks of that file also detail what a policy is.
class DesignatedWrapPolicy(MappedWrapPolicy):
"""A policy which uses a mapping to link functions and dispid
A MappedWrappedPolicy which allows the wrapped object to specify, via certain
special named attributes, exactly which methods and properties are exposed.
All a wrapped object need do is provide the special attributes, and the policy
will handle everything else.
Attributes:
_public_methods_ -- Required, unless a typelib GUID is given -- A list
of strings, which must be the names of methods the object
provides. These methods will be exposed and callable
from other COM hosts.
_public_attrs_ A list of strings, which must be the names of attributes on the object.
These attributes will be exposed and readable and possibly writeable from other COM hosts.
_readonly_attrs_ -- A list of strings, which must also appear in _public_attrs. These
attributes will be readable, but not writable, by other COM hosts.
_value_ -- A method that will be called if the COM host requests the "default" method
(ie, calls Invoke with dispid==DISPID_VALUE)
_NewEnum -- A method that will be called if the COM host requests an enumerator on the
object (ie, calls Invoke with dispid==DISPID_NEWENUM.)
It is the responsibility of the method to ensure the returned
object conforms to the required Enum interface.
_typelib_guid_ -- The GUID of the typelibrary with interface definitions we use.
_typelib_version_ -- A tuple of (major, minor) with a default of 1,1
_typelib_lcid_ -- The LCID of the typelib, default = LOCALE_USER_DEFAULT
_Evaluate -- Dunno what this means, except the host has called Invoke with dispid==DISPID_EVALUATE!
See the COM documentation for details.
"""
Up until now all the Python COM gateway classes on this blog have used late binding with the public methods listed in _public_methods_ . The doc string says that for early binding this will not be required but I will still use _public_methods_ to tell which methods to place in the type library. So I retain _public_methods_ (contrary to the documentation).
Official sample, pippo.py
The official code sample given to us by the great Mark Hammond (eternal thanks). The code sample consists of an IDL file, pippo.idl and a Python script implementing the COM server, pippo_server.py. Here is the pippo class
class CPippo:
#
# COM declarations
#
_reg_clsid_ = "{05AC1CCE-3F9B-4d9a-B0B5-DFE8BE45AFA8}"
_reg_desc_ = "Pippo Python test object"
_reg_progid_ = "Python.Test.Pippo"
#_reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
###
### Link to typelib
_typelib_guid_ = '{41059C57-975F-4B36-8FF3-C5117426647A}'
_typelib_version_ = 1, 0
_com_interfaces_ = ['IPippo']
def __init__(self):
self.MyProp1 = 10
def Method1(self):
return wrap(CPippo())
def Method2(self, in1, inout1):
return in1, inout1 * 2
And the given idl is thus
[
object,
uuid(F1A3CC2E-4B2A-4A81-992D-67862076949B),
dual,
helpstring("IPippo Interface"),
pointer_default(unique)
]
interface IPippo : IDispatch
{
[id(1), helpstring("method Method1")] HRESULT Method1([out, retval] IPippo **val);
[propget, id(2), helpstring("property MyProp1")] HRESULT MyProp1([out, retval] long *pVal);
[id(3), helpstring("method Method2")] HRESULT Method2([in] long in1, [in, out] long *inout1,
[out, retval] long *val);
};
But as I said above, as given one would have to maintain the class and the IDL file in synchronization which is a little painful. So now I can add a little value here and give some code which will read a Python class and then write and compile an IDL file into a type library that is in sync which the original Python class.
My test class, FooBar (housed in AComGatewayClass.py)
So here is my test class call FooBar. I have added some type annotations to demonstrate these being defined in the type library. I have placed this into a script file called AComGatewayClass.py. To import one would write from AComGatewayClass import FooBar.
We still have a _public_methods_ attribute even though that is more for late-binding; I use it to determine which methods to write to the type library.
Also, I have invented a new attribute called _reg_itfid_ which is use to snap (fix) the guid of the interface, so don't expect official documentation for that!
class FooBar(object):
_typelib_guid_ = "{92F288D0-4863-4030-A4EE-36DE63DB7664}"
_typelib_version_ = 1,0
_typelib_lcid_ = 0
_reg_clsid_ = "{8B994B6B-0865-4D48-8A62-2EB97C291BDA}"
_reg_itfid_ = "{B8FFDEFA-3EFB-4725-8CDD-1F6A9E35DD7C}" ### I have invented this to snap the interface's guid the type library
_reg_progid_ = 'MyPythonProject2.FooBar'
_com_interfaces_ = ["_FooBar"]
_reg_policy_spec_ = "DesignatedWrapPolicy" ### not strictly required as is already the default so key driver of functionality
_public_attrs_ = ['MyProp1']
_public_methods_ = ['Sum','NoArgs','Baz','Benjy']
def __init__(self):
self.MyProp1 = 10
def Sum(self,a:float, b:float)->float:
return a+b
def NoArgs(self) :
pass
def Baz(self,someString) -> str :
someBoolean:bool=True
if someBoolean:
return "Hello " + someString
else:
return "Goodbye " + someString
def Benjy(self,someInt:int, someDouble:float, untyped) -> int:
pass
def RegisterThis():
print("Registering COM servers...")
import win32com.server.register
win32com.server.register.UseCommandLine(FooBar)
if __name__ == '__main__':
RegisterThis()
IdlWriter.py
So here is the code that will take a Python class and create a type library for it. It requires the MIDL launcher I gave in the previous post and I saved to a script file MidlLauncherHelper.py . The code has a little extra logic to examine an argument type and give the correct type in the IDL which carries through to the type library. There isn't a huge amount to see, it is all string concatenation to be honest.
import pythoncom
from MidlLauncherHelper import MidlLauncher
class IdlWriter(object):
def PythonTypeToIDLType(self,annotations,argName:str) -> str:
try:
if argName in annotations:
pythonargtype = annotations[argName]
key2 = pythonargtype.__name__
return {
'bool': "VARIANT_BOOL",
'str':"BSTR*",
'float':"double",
'int':"long",
'':"VARIANT*",
}[key2]
else:
return "VARIANT*"
except Exception as e:
print("Error: " + str(e) + "\n")
def IdlFullFilename(self,idlFilename:str)->str:
import os
try:
this_dir = os.path.dirname(__file__)
return os.path.abspath(os.path.join(this_dir, idlFilename))
except Exception as e:
print("Error: " + str(e) + "\n")
def Main(self,library:str,typelib_guid,typelib_version,coclasses,idlFilename:str):
try:
IdlFullFilename = self.IdlFullFilename(idlFilename)
idlSrc = self.InspectMyClass(library,typelib_guid,typelib_version,coclasses)
with open(IdlFullFilename, "w+") as f:
f.write(idlSrc)
MidlLauncher.CompileTypelib(IdlFullFilename)
except Exception as e:
print("Error: " + str(e) + "\n")
def InspectMyClass(self,library:str,typelib_guid,typelib_version,coclasses):
import inspect
import uuid
try:
idl = "// Generated .IDL file (by Python code)\n//\n// typelib filename: FooBar.tlb\n"
idl = idl + "import \"oaidl.idl\";\nimport \"ocidl.idl\";\n"
idl = idl + "import \"unknwn.idl\";\n"
idl = idl + "[\n uuid(" + typelib_guid[1:-1] + "),\n version(" + str(typelib_version[0]) + "." + str(typelib_version[1]) + ")\n]\n"
idl = idl + "library " + library + "\n{\n"
idl = idl + " // TLib : // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}\n"
idl = idl + " importlib(\"stdole32.tlb\");\n"
idl = idl + " importlib(\"stdole2.tlb\");\n\n"
idl = idl + " importlib(\"stdole2.tlb\");\n\n"
idl = idl + " // Forward declare all types defined in this typelib\n"
for coclass in coclasses:
idl = idl + " interface " + "_" + coclass.__name__ + ";\n"
idl = idl + "\n"
for coclass in coclasses:
### rewritten to mimic pippo.idl in the win32com\test directory
idl = idl + " [\n"
idl = idl + " object,\n"
idl = idl + " uuid(" + coclass._reg_itfid_[1:-1] + "),\n"
idl = idl + " dual,\n"
idl = idl + " helpstring(\"test\"),\n"
idl = idl + " pointer_default(unique)\n"
idl = idl + " ]\n"
idl = idl + " interface " + "_" + coclass.__name__ + " : IDispatch {\n"
method_list2 = inspect.getmembers(coclass, inspect.isfunction)
dispid = 1 #start from 1 as zero equates to default member
for meth in method_list2 :
if meth[0] in coclass._public_methods_:
idl = idl + " [id(" + str(dispid) + ")]\n"
idl = idl + " HRESULT " + meth[0] + "(\n"
fullArgSpec = inspect.getfullargspec(meth[1])
argc = len(fullArgSpec.args)
for argIdx in range(1, argc):
arg = fullArgSpec.args[argIdx]
argType = self.PythonTypeToIDLType(fullArgSpec.annotations,arg)
idl = idl + " \t\t[in] " + argType + " " + arg + ",\n"
argType = self.PythonTypeToIDLType(fullArgSpec.annotations,"return")
idl = idl + " \t\t[out, retval] " + argType + "* retval );\n"
dispid = dispid + 1
if hasattr(coclass,'_public_attrs_'):
for attr in coclass._public_attrs_:
idl = idl + " [id(" + str(dispid) + "), propget]\n"
idl = idl + " HRESULT " + attr + "([out, retval] VARIANT *pVal);\n"
idl = idl + " [id(" + str(dispid) + "), propput]\n"
idl = idl + " HRESULT " + attr + "([in] VARIANT rhs);\n"
idl = idl + "\n };\n\n"
idl = idl + " [\n uuid(" + coclass._reg_clsid_[1:-1] + "),\n version(1.0)\n ]\n"
idl = idl + " coclass " + coclass.__name__ + "{\n"
idl = idl + " [default] interface " + "_" + coclass.__name__ + ";\n"
idl = idl + " };\n"
idl = idl + "};\n"
print(idl)
return idl
except Exception as e:
print("Error: " + str(e) + "\n")
if __name__ == '__main__':
from AComGatewayClass import FooBar
idl = IdlWriter()
idl.Main("MyPythonProject2",FooBar._typelib_guid_,FooBar._typelib_version_,[FooBar],"MyPythonProject2.idl")
print("End of execution")
By the way, you can pass in more than one class, it is written to take a list of classes.
VBA Calling Code
So now we can New a Python class in VBA thus ...
Sub TestEarlyBound()
On Error GoTo ErrHandler
Dim obj As MyPythonProject2.FooBar
Set obj = New MyPythonProject2.FooBar
Debug.Print obj.Sum(1, -2)
obj.MyProp1 = 256
Stop ' take a moment to admire the property MyProp1 in the Locals window, this would NOT appear without a type library
Exit Sub
ErrHandler:
Stop
End Sub
Enjoy!
Just wanted to point out that this discussion (https://stackoverflow.com/questions/35782404/registering-a-com-without-admin-rights) gives the registry keys required to register the server without admin rights. Very helpful in my work environment.
ReplyDelete