4

I need to determine if Excel is ready to accept a COM object from win32com in Python. For instance if a dialog box is open in excel, any call to a win32com function will cause a 'Call was rejected by callee' error.

Through trial and error I have found that if Excel ( actaully I assume any Office product ) has a dialog box open, any call to win32com will result in an error.

After quite a bit of googling I have found many questions about what amounts to self inflicted dialog boxes being open. i.e. doing a Excel.SaveAs() will open a dialog box on the sheet and you are stuck until the user closes it.

In my case I have a user that has opened a dialog box or in some otherway has interacted with Excel and left it waiting for input. Something as simple a starting to enter a formula on the formula bar will cause the win32com function to return with an error.

So couple of questions: Is there a way to determine if Excel is ready for a command? Is there a way to tell what box is open (what is excel waiting for?) Is there a way to close the box through win32com ... remember that as far as I can tell anything I do with win32com will return an error when it is in this state

I know I could do a try: catch: but I would need that around every win32com function ( at this point there are a lot of them). I think that that approach would make the code unnecessarily long and complex.

Burtski
  • 451
  • 5
  • 19
  • So it turns out that a user can open a dialog box on ANY excel sheet and you will have the same problem. That is, if another instance of Excel is being used by the user it is possible that an open dialog box on an unrelated sheet will cause the same problem. – Burtski Apr 30 '20 at 18:11

1 Answers1

6

I have been struggling with the same problem, but now I have made a solution that works for me so far.

I created a class, ComWrapper, that I wrap the Excel COM object in. It automatically wraps every nested object and call in ComWrapper, and unwraps them when they are used as arguments to function calls or assignments to wrapped objects. The wrapper works by catching the "Call was rejected by callee"-exceptions and retrying the call until the timeout defined at the top is reached. If the timeout is reached, the exception is finally thrown outside the wrapper object.

Function calls to wrapped objects are automatically wrapped by a function _com_call_wrapper, which is where the magic happens.

To make it work, just wrap the com object from Dispatch using ComWrapper and then use it as usual, like at the bottom of the code. Comment if there are problems.

import win32com.client
from pywintypes import com_error
import time
import logging

_DELAY = 0.05  # seconds
_TIMEOUT = 60.0  # seconds


def _com_call_wrapper(f, *args, **kwargs):
    """
    COMWrapper support function. 
    Repeats calls when 'Call was rejected by callee.' exception occurs.
    """
    # Unwrap inputs
    args = [arg._wrapped_object if isinstance(arg, ComWrapper) else arg for arg in args]
    kwargs = dict([(key, value._wrapped_object)
                   if isinstance(value, ComWrapper)
                   else (key, value)
                   for key, value in dict(kwargs).items()])

    start_time = None
    while True:
        try:
            result = f(*args, **kwargs)
        except com_error as e:
            if e.strerror == 'Call was rejected by callee.':
                if start_time is None:
                    start_time = time.time()
                    logging.warning('Call was rejected by callee.')

                elif time.time() - start_time >= _TIMEOUT:
                    raise

                time.sleep(_DELAY)
                continue

            raise

        break

    if isinstance(result, win32com.client.CDispatch) or callable(result):
        return ComWrapper(result)
    return result


class ComWrapper(object):
    """
    Class to wrap COM objects to repeat calls when 'Call was rejected by callee.' exception occurs.
    """

    def __init__(self, wrapped_object):
        assert isinstance(wrapped_object, win32com.client.CDispatch) or callable(wrapped_object)
        self.__dict__['_wrapped_object'] = wrapped_object

    def __getattr__(self, item):
        return _com_call_wrapper(self._wrapped_object.__getattr__, item)

    def __getitem__(self, item):
        return _com_call_wrapper(self._wrapped_object.__getitem__, item)

    def __setattr__(self, key, value):
        _com_call_wrapper(self._wrapped_object.__setattr__, key, value)

    def __setitem__(self, key, value):
        _com_call_wrapper(self._wrapped_object.__setitem__, key, value)

    def __call__(self, *args, **kwargs):
        return _com_call_wrapper(self._wrapped_object.__call__, *args, **kwargs)

    def __repr__(self):
        return 'ComWrapper<{}>'.format(repr(self._wrapped_object))


_xl = win32com.client.dynamic.Dispatch('Excel.Application')
xl = ComWrapper(_xl)

# Do stuff with xl instead of _xl, and calls will be attempted until the timeout is
# reached if "Call was rejected by callee."-exceptions are thrown.
  • Hello! While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value. – Valentino Apr 28 '19 at 17:03
  • @Valentino I added some context before the code. I hope it helps! – Erik Sällström Apr 28 '19 at 17:51
  • tried to use your code. Got error: AttributeError: '' object has no attribute 'Add' when calling: self.xlApp.Workbooks.Add() where: self._xl = win32com.client.gencache.EnsureDispatch('Excel.Application') self.xlApp = ComWrapper(self._xl) – Alex Jan 07 '20 at 09:39