Search code examples
pythonexceptiontornadodecorator

Python, Tornado: gen.coroutine decorator breaks try-catch in another decorator


I have a class with plenty static methods with Tornado coroutine decorator. And I want to add another decorator, to catch exceptions and write them to a file:

# my decorator
def lifesaver(func):
    def silenceit(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as ex:
            # collect info and format it
            res = ' ... '
            # writelog(res)
            print(res)
            return None

    return silenceit

However, it doesn't work with gen.coroutine decorator:

class SomeClass:

    # This doesn't work!
    # I tried to pass decorators in different orders,
    # but got no result.
    @staticmethod
    @lifesaver
    @gen.coroutine
    @lifesaver
    def dosomething1():
        raise Exception("Test error!")


    # My decorator works well
    # if it is used without gen.coroutine.
    @staticmethod
    @gen.coroutine
    def dosomething2():
        SomeClass.dosomething3()

    @staticmethod
    @lifesaver
    def dosomething3():
        raise Exception("Test error!")

I understand, that Tornado uses raise Return(...) approach which is probably based on Exceptions, and maybe it somehow blocks try-catches of other decorators... So, how can I used my decorator to handle Exceptions with Tornado coroutines?

The answer

Thanks to Martijn Pieters, I got this code working:

def lifesaver(func):
    def silenceit(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except (gen.Return, StopIteration):
            raise
        except Exception as ex:
            # collect info and format it
            res = ' ... '
            # writelog(res)
            print(res)

            raise gen.Return(b"")

    return silenceit

So, I only needed to specify Tornado Return. I tried to add @gen.coroutine decorator to silenceit function and use yield in it, but this leads to Future objects of Future objects and some other strange unpredictable behaviour.


Solution

  • You are decorating the output of gen.coroutine, because decorators are applied from bottom to top (as they are nested inside one another from top to bottom).

    Rather than decorate the coroutine, decorate your function and apply the gen.coroutine decorator to that result:

    @gen.coroutine
    @lifesaver
    def dosomething1():
        raise Exception("Test error!")
    

    Your decorator can't really handle the output that a @gen.coroutine decorated function produces. Tornado relies on exceptions to communicate results (because in Python 2, generators can't use return to return results). You need to make sure you pass through the exceptions Tornado relies on. You also should re-wrap your wrapper function:

    from tornado import gen
    
    def lifesaver(func):
        @gen.coroutine
        def silenceit(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except (gen.Return, StopIteration):
                raise
            except Exception as ex:
                # collect info and format it
                res = ' ... '
                # writelog(res)
                print(res)
                raise gen.Return(b"")
    
        return silenceit
    

    On exception, an empty Return() object is raised; adjust this as needed.

    Do yourself a favour and don't use a class just put staticmethod functions in there. Just put those functions at the top level in the module. Classes are there to combine methods and shared state, not to create a namespace. Use modules to create namespaces instead.