Search code examples
pythonpython-2.7compywin32

Add __getitem__ function to COM object in Python, OR: Overriding __slots__?


Goal

I'm working with pywin32 and win32com to manipulate COM objects from a third-party application.

One of my objects is a list, and I would like to iterate over the values in a list comprehension.

What works

With some of the COM objects I am able to successfully do things like:

>>> [obj.Name for obj in myCom.someList if 'Test' in obj.Name]

And, as expected, I get a list of results:

['Test1', 'Test2', 'Test3']

What doesn't...

However, on other similar list objects I try the same thing:

>>> [obj.Name for obj in myCom.anotherList]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "c:\Python27\lib\site-packages\win32com\gen_py\<class_string>.py", line 6575, in __iter__
    raise TypeError("This object does not support enumeration")
TypeError: This object does not support enumeration

What I've tried

So I try to add the function after defining my_iter:

>>> myCom.anotherList.__iter__ = my_iter
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "c:\Python27\lib\site-packages\win32com\client\__init__.py", line 473, in __setattr__
    raise AttributeError("'%s' object has no attribute '%s'" % (repr(self), attr))
AttributeError: '<win32com.gen_py.Thingy instance at 0xDEADBEEF>' object has no attribute '__iter__'

I tried the same thing with __getitem__ and next and I get the same error. I did some reading and I assumed that this doesn't work because of something to do with __slots__... Not sure.

Examining generated Python source

After examining .py shown in the first stack trace, I looked at the definition of class ISomeList.__iter__ - since that worked perfectly in the first example in the post.

from win32com.client import DispatchBaseClass
LCID = 0x0
class ISomeList(DispatchBaseClass):
    def __iter__(self):
        "Return a Python iterator for this object"
        try:
            ob = self._oleobj_.InvokeTypes(-4,LCID,2,(13, 10),())
        except pythoncom.error:
            raise TypeError("This object does not support enumeration")
        return win32com.client.util.Iterator(ob, '{<<<guid>>>}')

And compare that to class IAnotherList.__init__:

class IAnotherList(DispatchBaseClass):
    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, None)

So, to me, it looks like assigning to ob fails and then we raise an error.

I can modify the generated source to add the __iter__ method, but that seems like a very "works on my machine" solution. What can I do?


Solution

  • The solution

    I'm very open to any alternative approaches, but this is what I came up with on my own.

    I discovered that there is a win32com helper function called GetModuleForProgID - as you might expect from the name, it will take the COM Program ID and return the pywin32 module that was generated to wrap that object.

    The code

    What ended up working best for me was:

    """ define a method to do my iteration """
    def com_generic_iter(self):
        current = 1 """ All the com objects I'm working with are 1-based indeces """
        while current <= self.Count:
            yield self.Item(current)
            current += 1
    
    """ I also wanted to support __getitem__ indexing: """
    def generic_getitem(self, k):
        return self.Item(k)
    
    """ Then dynamically add the method to the application wrapper when I generate my object """
    mod = win32com.client.gencache.GetModuleForProgID("MyProgram.Application")
    mod.IAnotherList.__iter__ = com_generic_iter
    mod.IAnotherList.__getitem__ = generic_getitem
    app = win32com.client.Dispatch("MyProgram.Application")
    
    print [x.Name for x in app.AnotherList]
    
    """
    [<win32com.gen_py.MyProgram.IItem instance at 0x12345678>,
     <win32com.gen_py.MyProgram.IItem instance at 0x87654321>,
     <win32com.gen_py.MyProgram.IDifferentItem instance at 0x99999999>]
    """
    
    print app.AnotherList[1]
    
    """
    <win32com.gen_py.MyProgram.IItem instance at 0x12345678>
    """
    

    Edit:

    You can also add __iter__ and __getitem__ with setattr like this:

    mod = win32com.client.gencache.GetModuleForProgID("MyProgram.Application")
    IAnotherList = mod.IAnotherList
    setattr(IAnotherList, '__getitem__', generic_getitem)   
    setattr(IAnotherList, '__iter__', com_generic_iter)
    

    Sources

    I used win32com documentation to find details about GetModuleForProgID.