Search code examples
pythonvisiowin32com

Override setter by property in win32com


I'm using win32com to control Visio from Python.

Getting and setting shapesheet values is as easy as:

print(shp.CellsU('PinX').ResultStr(''))
# and
shp.CellsU('PinX').FormulaU = '1'

So far so good, but I want a shorter syntax by overriding the setter and getter to get something like:

print(shp.PinX)
# and
shp.PinX = '1'

So I went for a property:

ShapeClass = type(shp)

def SetPinX(self,value):
    self.CellsU('PinX').FormulaU = value

def GetPinX(self):
    return self.CellsU('PinX').ResultStr('')

ShapeClass.PinX = property(GetPinX,SetPinX)

Now the strange result - The getter works just fine (print(shp.PinX) give the expected value), but the setter won't work.

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~\AppData\Local\conda\conda\envs\YG_Package_1\lib\site-packages\win32com\client\__init__.py in __setattr__(self, attr, value)
    478                 try:
--> 479                         args, defArgs=self._prop_map_put_[attr]
    480                 except KeyError:

KeyError: 'PinX'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
<ipython-input-28-23f68b65624d> in <module>()
----> 1 shp.PinX= '1'

~\AppData\Local\conda\conda\envs\YG_Package_1\lib\site-packages\win32com\client\__init__.py in __setattr__(self, attr, value)
    479                         args, defArgs=self._prop_map_put_[attr]
    480                 except KeyError:
--> 481                         raise AttributeError("'%s' object has no attribute '%s'" % (repr(self), attr))
    482                 self._oleobj_.Invoke(*(args + (value,) + defArgs))
    483         def _get_good_single_object_(self, obj, obUserName=None, resultCLSID=None):

AttributeError: '<win32com.gen_py.Microsoft Visio 15.0 Type Library.IVShape instance at 0x85710888>' object has no attribute 'PinX'

dir(ShapeClass) shows the attribute PinX just fine.

Testing with an own class also worked. So that the error is not in the way I'm implementing the property.

I suspect win32com to have problems with setters being overridden.

Would anyone have an idea on how to solve the problem?


Solution

  • The win32com.client.DispatchBaseClass base class uses __setattr__ to intercept all attribute setting access. This affects your property object; property setters only get called by the default object.__setattr__ implementation, not by the custom method used by win32com.

    So yes, shp.PinX = '1' will call DispatchBaseClass.__setattr__('PinX', '1'), even if there is a data descriptor called PinX defined on the class, and that'll fail as that only supports attributes defined by the COM interface.

    You'll have to override the __setattr__ method here to check for available properties first. You could either subclass DispatchBaseClass or specific generated classes, or we could just monkey-patch win32com directly:

    import inspect
    from win32com.client import DispatchBaseClass
    
    dispatch_setattr = DispatchBaseClass.__setattr__
    
    def allow_datadescriptor_setattr(self, name, value):
        # for non-private names, check if the attribute exists on the class
        # and is a data descriptor. If so, use object.__setattr__ to permit
        # the data descriptor to intercept attribute setting
        if name[:1] != '_' and inspect.isdatadescriptor(getattr(type(self), name, None)):
            return object.__setattr__(self, name, value)
        # private name, or doesn't exist on the class, or not a data descriptor
        # invoke the original win32com dispatch __setattr__
        dispatch_setattr(self, name, value)
    
    DispatchBaseClass.__setattr__ = allow_datadescriptor_setattr
    

    The above allows any descriptor with a __set__ or __delete__ method to intercept assignments to their name, not just property objects.