1

I have prepared some function decorator with default argument passed from decorator point of view:

def decorator_wraper(max_repeating=20)
    def decorator(function):
        def wrap_function(*args, **kwargs):
            print(f"max_repeating = {max_repeating}")
            repeat = 0
            while repeat < max_repeating:
                try:
                    return function(*args, **kwargs)
                    break
                except Exception:
                    pass
                repeat += 1
        return wrap_function
    return decorator

It works ok, so if some decorated function fails - decorator allows to repeat the function until success or repeat >= max_repeating.

But what if I will need to change the default max_repeating from decorated function point of view?

Here I have two decorated functions:

@decorator_wraper(5)
def decorate_me_with_argument(max_repeating=10):
    print('decorate_me_with_argument')

@decorator_wraper()
def decorate_me_with_default():
    print('decorate_me_with_default')

calling:
decorate_me_with_argument() # max_repeating should be 5 cause while decorating the function I have passed 5.
decorate_me_with_default() # max repeating should be 20 cause default for the decorator is 20.

And what I want to achieve:

decorate_me_with_argument(3) # max_repeating should be 3
decorate_me_with_default(8) # max repeating should be 8

Maybe is there any simple method to solve something like this?

tomm
  • 271
  • 2
  • 14
  • 1
    Any arguments you pass to `decorate_me_with_argument` are now going to `wrap_function`, what if the wrapped `function` also takes arguments? – jonrsharpe Sep 29 '21 at 10:29
  • I have forced all arguments as key-args and it's working for me. (I mean solution what big_bad_bison proposed) Maybe do you have any better solution? – tomm Sep 29 '21 at 13:47

3 Answers3

1

Make the first argument for wrap_function the number of repeats with the default value of max_repeats specified in the decorator.

def decorator_wraper(max_repeats=20):
    def decorator(function):
        def wrap_function(max_repeating=max_repeats, *args, **kwargs):
            print(f"max_repeating = {max_repeating}")
            repeat = 0
            while repeat < max_repeating:
                try:
                    return function(*args, **kwargs)
                except Exception:
                    pass
        return wrap_function
    return decorator


@decorator_wraper(5)
def decorate_me_with_argument():
    print('decorate_me_with_argument')


@decorator_wraper()
def decorate_me_with_default():
    print('decorate_me_with_default')


decorate_me_with_argument()  # max_repeating = 5
decorate_me_with_default()  # max_repeating = 20
decorate_me_with_argument(3)  # max_repeating = 3
decorate_me_with_default(8)  # max_repeating = 8
big_bad_bison
  • 1,021
  • 1
  • 7
  • 12
1

The other way to do this is to expose the number of repeats on the wrap_function:

def decorator_wrapper(max_repeating=20):
    def decorator(function):
        def wrap_function(*args, **kwargs):
            print(f"max_repeating = {wrap_function.max_repeating}")
            for _ in range(wrap_function.max_repeating):
                try:
                    return function(*args, **kwargs)
                except Exception:
                    pass
        wrap_function.max_repeating = max_repeating
        return wrap_function
    return decorator

That way you don't change the interface of the wrapped function, but you can still change the repeat limit after the initial decoration:

>>> @decorator_wrapper()
... def bad_func():
...     print("in bad func")
...     raise Exception("oh no!")
...
>>> bad_func.max_repeating
20
>>> bad_func.max_repeating = 3
>>> bad_func()
max_repeating = 3
in bad func
in bad func
in bad func

Adding arbitrary attributes to wrap_function would also allow you to provide an inline API for calling the wrapped function with a different number of retries, e.g. something like:

bad_func.retry_times(3)()

Again bad_func.retry_times(3) is a drop-in replacement for bad_func as it returns a function that accepts exactly the same arguments, rather than adding to (and risking clashing with) the wrapped function's existing parameters.


Note you should probably raise the error if the last retry fails, or it will get lost entirely:

def retry(times, func, *args, **kwargs):
    for _ in range(times):
        try:
            return func(*args, **kwargs)
        except Exception as exc:
            exception = exc
    raise exception

exception = exc is needed because except-clause deletes local variable.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • Oh yeah, of course repeat += 1 should be in the decorator. – tomm Sep 29 '21 at 14:17
  • 1
    @tomm I switched from the `while` loop to a `for`, so you don't need the `repeat` variable at all. – jonrsharpe Sep 29 '21 at 14:19
  • It's really interesting what did you add above. I have just some question to the above. What did you mean while adding an example "bad_func.retry_times(3)()"? How to use it? – tomm Sep 29 '21 at 15:14
  • 1
    @tomm 1. in your current implementation if the function fails _every time_ it's retried, the wrapper just silently returns and the error can't be handled; 2. I mean rather than adding a `max_repeating` attribute that's an integer, you could add a `retry_times` attribute that's a function. And that _is_ how to use it, you call the attribute to get a function with the right number of retries then call that function with whatever arguments are needed. – jonrsharpe Sep 29 '21 at 15:16
-1

Following is similar to what I have in my project, the kwargs param takes all what are passed from the decorated function:

def decorator_wraper(func):
    def wrap_function(max_repeating=20, *args, **kwargs):
        if kwargs.get('max_repeating'):
            max_repeating = kwargs['max_repeating']
        print(max_repeating)
        # TODO
        return func(*args, **kwargs)
    return wrap_function

@decorator_wraper
def decorate_me():
    pass

decorate_me()  # should print 20
decorate_me(max_repeating=10)  # should print 10
  • 1
    `max_repeating` is the first _positional_ argument to `wrap_function`, so if you decorated a function with any positional parameters you'd never be able to call it _without_ setting `max_repeating`. – jonrsharpe Sep 29 '21 at 14:15