简体   繁体   中英

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM