Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

win32com.client returns objects witht he wrong interface when using makepy generated modules #1699

Closed
dnicolodi opened this issue May 11, 2021 · 4 comments

Comments

@dnicolodi
Copy link

I am using Python 3.8.8 and pywin32 227 on Windows 10. I am trying to do some SAP GUI scripting in Python. All goes well when I don't generate the python modules for the COM interfaces and I rely on dynamic dispatch, however, things break when win32com.client invokes makepy.

A simple example to demonstrates the issue:

import win32com.client
sap = win32com.client.GetObject('SAPGUI')
application = sap.GetScriptingEngine
len(application.Connections)

works. However, after running makepy for the relevant COM module, the same code raises an exception:

  File "C:\Users\nicolo01\Documents\Python\examples\sap.py", line 34, in main
    len(application.Connections)
TypeError: object of type 'GuiComponentCollection' has no len()

Inspecting the module generated by makepy, the Connection property is defined like this:

class _Dsapfewse(DispatchBaseClass):
        ...
	_prop_map_get_ = {
                ...
		# Property 'Connections' is an object of type 'GuiComponentCollection'                
		"Connections": (32900, 2, (13, 0), (), "Connections", '{A464F5B4-CC70-4062-A0A1-6DC9B461D776}'),
                ...
        }

which I think corresponds to this class:

class GuiComponentCollection(CoClassBaseClass): # A CoClass
	CLSID = IID('{A464F5B4-CC70-4062-A0A1-6DC9B461D776}')
	coclass_sources = [
	]
	coclass_interfaces = [
		ISapCollectionTarget,
	]
	default_interface = ISapCollectionTarget

and this interface:

class ISapCollectionTarget(DispatchBaseClass):
	CLSID = IID('{736C7684-ECDA-4984-87A9-AE358AE8823F}')
	coclass_clsid = IID('{A464F5B4-CC70-4062-A0A1-6DC9B461D776}')

	# Result is of type GuiComponent
	def ElementAt(self, Index=defaultNamedNotOptArg):
		ret = self._oleobj_.InvokeTypes(33102, LCID, 1, (13, 0), ((3, 0),),Index
			)
		if ret is not None:
			# See if this IUnknown is really an IDispatch
			try:
				ret = ret.QueryInterface(pythoncom.IID_IDispatch)
			except pythoncom.error:
				return ret
			ret = Dispatch(ret, 'ElementAt', '{ABCC907C-3AB1-45D9-BF20-D3F647377B06}')
		return ret

	# Result is of type GuiComponent
	def Item(self, Index=defaultNamedNotOptArg):
		ret = self._oleobj_.InvokeTypes(0, LCID, 1, (13, 0), ((12, 0),),Index
			)
		if ret is not None:
			# See if this IUnknown is really an IDispatch
			try:
				ret = ret.QueryInterface(pythoncom.IID_IDispatch)
			except pythoncom.error:
				return ret
			ret = Dispatch(ret, 'Item', '{ABCC907C-3AB1-45D9-BF20-D3F647377B06}')
		return ret

	_prop_map_get_ = {
		"Count": (33100, 2, (3, 0), (), "Count", None),
		"Length": (33101, 2, (3, 0), (), "Length", None),
		"Type": (32015, 2, (8, 0), (), "Type", None),
		"TypeAsNumber": (32032, 2, (3, 0), (), "TypeAsNumber", None),
	}
	_prop_map_put_ = {
		"Count" : ((33100, LCID, 4, 0),()),
		"Length" : ((33101, LCID, 4, 0),()),
		"NewEnum" : ((-4, LCID, 4, 0),()),
		"Type" : ((32015, LCID, 4, 0),()),
		"TypeAsNumber" : ((32032, LCID, 4, 0),()),
	}
	# Default method for this class is 'Item'
	def __call__(self, Index=defaultNamedNotOptArg):
		ret = self._oleobj_.InvokeTypes(0, LCID, 1, (13, 0), ((12, 0),),Index
			)
		if ret is not None:
			# See if this IUnknown is really an IDispatch
			try:
				ret = ret.QueryInterface(pythoncom.IID_IDispatch)
			except pythoncom.error:
				return ret
			ret = Dispatch(ret, '__call__', '{ABCC907C-3AB1-45D9-BF20-D3F647377B06}')
		return ret

	def __str__(self, *args):
		return str(self.__call__(*args))
	def __int__(self, *args):
		return int(self.__call__(*args))
	def __iter__(self):
		"Return a Python iterator for this object"
		try:
			ob = self._oleobj_.InvokeTypes(-4,LCID,3,(13, 10),())
		except pythoncom.error:
			raise TypeError("This object does not support enumeration")
		return win32com.client.util.Iterator(ob, '{ABCC907C-3AB1-45D9-BF20-D3F647377B06}')
	#This class has Count() property - allow len(ob) to provide this
	def __len__(self):
		return self._ApplyTypes_(*(33100, 2, (3, 0), (), "Count", None))
	#This class has a __len__ - this is needed so 'if object:' always returns TRUE.
	def __nonzero__(self):
		return True

which seems to implement the right methods and Python interfaces (by the way, it would be cool if __getitem__ could also be implemented for COM interfaces exposing containers). However, I don't really understand how all the pieces are supposed to be glued together, and thus why it does not work as intended. The Connections property return the right type:

type(application.Connections)
win32com.gen_py.5EA428A0-F2B8-45E7-99FA-0E994E82B5BCx0x1x0.GuiComponentCollection

The same think happens for all other properties that return a COM object. I can go on with dynamic dispatching just fine, without the generated modules, but the generated modules are necessary (or at least they built is triggered by) using event dispatching, thus I am kind of stuck.

Please let me know if there is anything more I can provide to help debug the issue.

@dnicolodi
Copy link
Author

I think I found why the code does not work: https://docs.python.org/3/reference/datamodel.html#special-lookup Special methods are not looked up through __getattr__ which is used by CoClassBaseClass to do its magic. It is surprising that the machinery used by dynamic dispatch works instead.

It occurs to me, that to make the mechanism work, the property class type could be defined somehow similarly to this instead:

class GuiComponentCollection(ISapCollectionTarget):
	CLSID = IID('{A464F5B4-CC70-4062-A0A1-6DC9B461D776}')

but I admit that I didn't spend too much time analyzing what a CoClass does.

@mhammond
Copy link
Owner

Special methods are not looked up through __getattr__ which is used by CoClassBaseClass to do its magic.

If that wasn't true in 2.x, then it's probably a regression. I'd expect to find some tests for this in the pycomtest tests, but it's going to take me some time to dig in here. But yeah - CoClasses are kinda strange.

@dnicolodi
Copy link
Author

dnicolodi commented May 12, 2021

Assuming that you mean a Python 3.x vs 2.x regression, the behavior for what were "new style classes" in Python 2.x has not changed. However, "new style classes" were opt-in in Python 2.x while they are the only flavor of classes available in Python 3.x.

The less invasive solution would be to have the generated classes have __dunder__ methods that redirect to the wrapped object. The proper solution would probably be to revise how CoClasses are implemented.

In the meanwhile, is there a way to use COM events without the generated modules?

@mhammond
Copy link
Owner

Thanks - I pushed a work-around in 17fb589.

In the meanwhile, is there a way to use COM events without the generated modules?

Not that I'm aware of :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants