pythonpython-decoratorsname-collision

How to avoid name collisions in python decorators functions


I would like to write a python decorator so that a function raising an exception will be run again until either it succeeds, or it reaches the maximum number of attempts before giving up.

Like so :

def tryagain(func):
    def retrier(*args,**kwargs,attempts=MAXIMUM):
        try:
            return func(*args,**kwargs)
        except Exception as e:
            if numberofattempts > 0:
                logging.error("Failed. Trying again")
                return retrier(*args,**kwargs,attempts=attempts-1)
            else:
                logging.error("Tried %d times and failed, giving up" % MAXIMUM)
                raise e
    return retrier

My problem is that I want a guarantee that no matter what names the kwargs contain, there cannot be a collision with the name used to denote the number of attempts made.

however this does not work when the function itself takes attempts as a keyword argument

@tryagain
def other(a,b,attempts=c):
    ...
    raise Exception

other(x,y,attempts=z)

In this example,if other is run, it will run z times and not MAXIMUM times (note that for this bug to happen, the keyword argument must be explicitly used in the call !).


Solution

  • You can specify decorator parameter, something along the lines of this:

    import logging
    
    MAXIMUM = 5
    
    def tryagain(attempts=MAXIMUM):
        def __retrier(func):
            def retrier(*args,**kwargs):
                nonlocal attempts
                while True:
                    try:
                        return func(*args,**kwargs)
                    except Exception as e:
                        attempts -= 1
                        if attempts > 0:
                            print('Failed, attempts left=', attempts)
                            continue
                        else:
                            print('Giving up')
                            raise
            return retrier
        return __retrier
    
    
    @tryagain(5)                              # <-- this specifies number of attempts
    def fun(attempts='This is my parameter'): # <-- here the function specifies its own `attempts` parameter, unrelated to decorator
        raise Exception(attempts)
    
    fun()