42

I have some code where I try to reach a resource but sometimes it is unavailable, and results in an exception. I tried to implement a retry engine using context managers, but I can't handle the exception raised by the caller inside the __enter__ context for my context manager.

class retry(object):
    def __init__(self, retries=0):
        self.retries = retries
        self.attempts = 0
    def __enter__(self):
        for _ in range(self.retries):
            try:
                self.attempts += 1
                return self
            except Exception as e:
                err = e
    def __exit__(self, exc_type, exc_val, traceback):
        print 'Attempts', self.attempts

These are some examples which just raise an exception (which I expected to handle)

>>> with retry(retries=3):
...     print ok
... 
Attempts 1
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: name 'ok' is not defined
>>> 
>>> with retry(retries=3):
...     open('/file')
... 
Attempts 1
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
IOError: [Errno 2] No such file or directory: '/file'

Is there any way to intercept these exception(s) and handle them inside the context manager?

Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
Mauro Baraldi
  • 6,346
  • 2
  • 32
  • 43

5 Answers5

47

Quoting __exit__,

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

By default, if you don't return a value explicitly from a function, Python will return None, which is a falsy value. In your case, __exit__ returns None and that is why the exeception is allowed to flow past the __exit__.

So, return a truthy value, like this

class retry(object):

    def __init__(self, retries=0):
        ...


    def __enter__(self):
        ...

    def __exit__(self, exc_type, exc_val, traceback):
        print 'Attempts', self.attempts
        print exc_type, exc_val
        return True                                   # or any truthy value

with retry(retries=3):
    print ok

the output will be

Attempts 1
<type 'exceptions.NameError'> name 'ok' is not defined

If you want to have the retry functionality, you can implement that with a decorator, like this

def retry(retries=3):
    left = {'retries': retries}

    def decorator(f):
        def inner(*args, **kwargs):
            while left['retries']:
                try:
                    return f(*args, **kwargs)
                except NameError as e:
                    print e
                    left['retries'] -= 1
                    print "Retries Left", left['retries']
            raise Exception("Retried {} times".format(retries))
        return inner
    return decorator


@retry(retries=3)
def func():
    print ok

func()
ThatXliner
  • 414
  • 5
  • 15
thefourtheye
  • 233,700
  • 52
  • 457
  • 497
  • But I expected it returned "Attempts 3", not 1 – Mauro Baraldi Feb 18 '16 at 14:02
  • 1
    @MauroBaraldi That is not possible with context managers. You might want to use a decorator for that. – thefourtheye Feb 18 '16 at 14:04
  • 1
    @MauroBaraldi I included a sample program which retires. PTAL. – thefourtheye Feb 18 '16 at 14:23
  • Thanks for help, but i nee to use it only in a small portion of code not at all the method. In the end, it's is a workaround to fix another (uglier) workaround. :-( – Mauro Baraldi Feb 18 '16 at 14:54
  • 3
    @MauroBaraldi Well, context managers are, as the name says, just to manage the context for the particular block of code. They have no control over the control flow. So, decorator is the only viable/less-uglier option. – thefourtheye Feb 18 '16 at 14:56
15

To deal with an exception in an __enter__ method, the most straightforward (and less surprising) thing to do, would be to wrap the with statement itself in a try-except clause, and simply raise the exception -

But, with blocks are definetelly not designed to work like this - to be, by themselves "retriable" - and there is some misunderstanding here:

def __enter__(self):
    for _ in range(self.retries):
        try:
            self.attempts += 1
            return self
        except Exception as e:
            err = e

Once you return self there, the context were __enter__ runs no longer exists - if an error occurs inside the with block, it will just flow naturally to the __exit__ method. And no, the __exit__ method can not, in anyway, make the execution flow go back to the beginning of the with block.

You are probably wanting something more like this:

class Retrier(object):

    max_retries = 3

    def __init__(self, ...):
         self.retries = 0
         self.acomplished = False

    def __enter__(self):
         return self

    def __exit__(self, exc, value, traceback):
         if not exc:
             self.acomplished = True
             return True
         self.retries += 1
         if self.retries >= self.max_retries:
             return False
         return True

....

x = Retrier()
while not x.acomplished:
    with x:
        ...
mit
  • 11,083
  • 11
  • 50
  • 74
jsbueno
  • 99,910
  • 10
  • 151
  • 209
9

I think this one is easy, and other folks seem to be overthinking it. Just put the resource fetching code in __enter__, and try to return, not self, but the resource fetched. In code:

def __init__(self, retries):
    ...
    # for demo, let's add a list to store the exceptions caught as well
    self.errors = []

def __enter__(self):
    for _ in range(self.retries):
        try:
            return resource  # replace this with real code
        except Exception as e:
            self.attempts += 1
            self.errors.append(e)

# this needs to return True to suppress propagation, as others have said
def __exit__(self, exc_type, exc_val, traceback):
    print 'Attempts', self.attempts
    for e in self.errors:
        print e  # as demo, print them out for good measure!
    return True

Now try it:

>>> with retry(retries=3) as resource:
...     # if resource is successfully fetched, you can access it as `resource`;
...     # if fetching failed, `resource` will be None
...     print 'I get', resource
I get None
Attempts 3
name 'resource' is not defined
name 'resource' is not defined
name 'resource' is not defined
gil
  • 2,086
  • 12
  • 13
6

I found contextmanager from contextlib useful, hope this may be helpful.

from contextlib import contextmanager

@contextmanager
def handler(*args, **kwargs):
  try:
      # print(*args, **kwargs)
      yield
  except Exception:
      # Handle exception     

Now, to use it,

# Add optional args or kwargs
with handler():
  # Code with probable exception
  print("Hi")
Henshal B
  • 1,540
  • 12
  • 13
1

You don't have to implement the retry functionality manually. Take a look at the tenacity library.

Tenacity is a general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything.

You can simply add @retry decorator with parameters to your function.

Also,

Tenacity allows you to retry a code block without the need to wraps it in an isolated function. The trick is to combine a for loop and a context manager.