tybur
tybur

Reputation: 158

Responding to a global object's exception in Python

I am new to Python and I stumbled upon a problem with exception handling. I'm writing a simple tornado + momoko application. In summary, I have a global (?) object that is created within the main function, the object is of class QueryExecutor. It's a simple class that handles SQL query execution using momoko.

class QueryExecutor:
    def __init__(self, database):
        self.db = database

    @gen.engine
    def _run(self, query):
        self.db.execute(query, callback = (yield gen.Callback('q')))
        try:
            cursor = yield momoko.WaitOp('q')
        except Exception as error:
            print(str(error))

    def save(self, tablename, data):
        fields = "(" + ", ".join(map(str, list(data.keys()))) + ")"
        values = "(" + "\'" + '\', \''.join(map(str, list(data.values()))) + "\'" + ")"
        query = "INSERT INTO " + tablename + " " + fields + " VALUES " + values + ";"
        self._run(query)

What I want to achieve is to use an object of this class inside request handlers and to somehow be able to tell when an exception has occurred:

class RegistrationHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("templates/register.html")

    def post(self):
        #...handle post arguments, retrieve user data, check if correct etc., do stuff...
        #...if everything ok:
        queryExec.save("users", userdata)
        #what to do if save threw an exception? 


application.db = momoko.Pool(dsn=dbsetup.dsn, size=1)


if __name__ == "__main__":
    queryExec = dbsetup.QueryExecutor(database=application.db)
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

queryExec.save() throws an exception when the query fails, and I would like to know inside the request handler function if it happens. Embedding queryExec.save() in try and except blocks does not work. Obviously I can probably pass additional parameters (references?) to queryExec.save(), or add some kind of a state parameter to the QueryExecutor class itself, but I'm wondering if there is a more elegant way to solve this?

EDIT: After some modifications:

class TestEx(Exception): pass

and:

@gen.engine
def _run(self, query):
    self.db.execute(query, callback = (yield gen.Callback('q')))
    try:
        cursor = yield momoko.WaitOp('q')
    except Exception as error:
        print(str(error))
        raise TestEx("test exception")

and:

try:
    queryExec.save("users", userdata)
except dbsetup.TestEx as ex:
    print("exception caught in caller function")
    self.redirect("templates/login.html")

I get in the console:

/usr/bin/python3.3 /home/tybur/PycharmProjects/tornadochat/main.py
duplicate key value violates unique constraint "unique_names"
DETAIL:  Key (name)=(testuser) already exists.

ERROR:tornado.application:Exception in callback None
Traceback (most recent call last):
  File "/home/tybur/PycharmProjects/tornadochat/dbsetup.py", line 46, in _run
    cursor = yield momoko.WaitOp('q')
  File "/usr/local/lib/python3.3/dist-packages/tornado/gen.py", line 520, in run
    next = self.yield_point.get_result()
  File "/usr/local/lib/python3.3/dist-packages/momoko/utils.py", line 59, in get_result
    raise error
  File "/usr/local/lib/python3.3/dist-packages/momoko/connection.py", line 244, in io_callback
    state = self.connection.poll()
psycopg2.IntegrityError: duplicate key value violates unique constraint "unique_names"
DETAIL:  Key (name)=(testuser) already exists.


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.3/dist-packages/tornado/ioloop.py", line 688, in start
    self._handlers[fd](fd, events)
  File "/usr/local/lib/python3.3/dist-packages/tornado/stack_context.py", line 331, in wrapped
    raise_exc_info(exc)
  File "<string>", line 3, in raise_exc_info
  File "/usr/local/lib/python3.3/dist-packages/tornado/stack_context.py", line 302, in wrapped
    ret = fn(*args, **kwargs)
  File "/usr/local/lib/python3.3/dist-packages/momoko/connection.py", line 248, in io_callback
    self.callback(error)
  File "/usr/local/lib/python3.3/dist-packages/tornado/stack_context.py", line 331, in wrapped
    raise_exc_info(exc)
  File "<string>", line 3, in raise_exc_info
  File "/usr/local/lib/python3.3/dist-packages/tornado/stack_context.py", line 302, in wrapped
    ret = fn(*args, **kwargs)
  File "/usr/local/lib/python3.3/dist-packages/tornado/gen.py", line 574, in inner
    self.set_result(key, result)
  File "/usr/local/lib/python3.3/dist-packages/tornado/gen.py", line 500, in set_result
    self.run()
  File "/usr/local/lib/python3.3/dist-packages/tornado/gen.py", line 529, in run
    yielded = self.gen.throw(*exc_info)
  File "/home/tybur/PycharmProjects/tornadochat/dbsetup.py", line 49, in _run
    raise TestEx("test exception")
dbsetup.TestEx: test exception

Upvotes: 0

Views: 417

Answers (1)

Ben Darnell
Ben Darnell

Reputation: 22154

save() does not raise an exception; it starts a call to _run but does not wait for it, so there is nowhere for the exception to go except to be logged. To fix this you should follow three rules:

  • Unless you have a specific reason to use @gen.engine, you should use @gen.coroutine instead, which makes things work a little more like normal functions.
  • Any method that calls a coroutine must also be a coroutine. This rule has exceptions, but they're subtle and until you have a better handle on asynchronous programming you should follow it. Since only a coroutine can call a coroutine, you can only start the chain at certain locations - usually methods that are described as "may return a Future" in the Tornado docs (which includes the RequestHandler get/post/etc methods).
  • Calls to coroutines should usually be prefixed with "yield" to wait for their results. If you don't use "yield" when you call a coroutine, you should save its return value (a Future, which is a placeholder for its actual result) and yield it later (you might want to do this to start several coroutines in parallel and wait for them all at once).

So in this case, make save(), _run() and post() coroutines, and use the yield keyword when calling _run() and save().

Upvotes: 1

Related Questions