31

Is it possible to await arbitrary calls to an async function when inside a Python debugger?

Say I have the following code in some main.py file:

import asyncio

async def bar(x):
    return x + 1

async def foo():
    import ipdb; ipdb.set_trace()

asyncio.run(foo())

Now I want to test calling bar() with some argument inside the debugger to test the results. The following happens:

$ python3 main.py
> /Users/user/test/main.py(8)foo()
      7     import ipdb; ipdb.set_trace()
----> 8     return None
      9

ipdb> bar(1)
<coroutine object bar at 0x10404ae60>
main.py:1: RuntimeWarning: coroutine 'bar' was never awaited
  import asyncio
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
ipdb> await bar(1)
*** SyntaxError: 'await' outside function

Of course, I can get around this by having x = await bar(1) above my ipdb.set_trace(), and then inspecting the results, but then I can't try calling my functions in real time while the debugger is active.

Ivan Gozali
  • 2,089
  • 1
  • 27
  • 25
  • The trouble with this is that when you reach your breakpoint, the event loop is already in the middle of processing an event. In order to get the result of `bar(1)`, you need to allow control to return to the event loop, pick up your task from the queue, and run it. The debugger can't do that while leaving your current call stack in place. It seems like you should be able to create a new, private event loop and use that, but asyncio won't let you invoke another event loop while one is already running – Matt Zimmerman Aug 19 '19 at 19:02
  • Yeah, I had a similar idea to start a new event loop to run async functions on that one, but had no idea how to do it. I do see the problem you mentioned, though, where the main event loop is suspended because of the `ipdb` interpreter waiting for our input, and it makes sense. I wish `ipdb` had a utility function to do this somehow. – Ivan Gozali Aug 20 '19 at 17:59
  • There is an open feature request for it, but there has been no activity: https://bugs.python.org/issue42045 – shadowtalker Apr 07 '21 at 15:13

2 Answers2

6

Here a modified version of what I posted at https://stackoverflow.com/a/67847257/893857 since it also solves this problem.

I found a solution using nest_asyncio. If one has the following async example script:

import asyncio
import nest_asyncio


async def bar(x):
    return x + 1

async def foo():
    import ipdb; ipdb.set_trace()


if __name__=="__main__":
    loop = asyncio.get_event_loop()
    nest_asyncio.apply(loop)
    loop.run_until_complete(foo())

One can then do:

      8 async def foo():
----> 9     import ipdb; ipdb.set_trace()
     10 

ipdb> loop = asyncio.get_event_loop()
ipdb> loop.run_until_complete(bar(1))
2

Admittedly it is a bit more tedious then await bar(1) but it gets the job done. Hopefully a more elegant solution will come up in the future.

Maarten Derickx
  • 1,502
  • 1
  • 16
  • 27
  • For me this is the most convenient solution. Especially in recent versions where `nest_asyncio.apply()` doesn't even need the `loop` parameter, which defaults to `asyncio.get_event_loop()`. – LeoRochael Jun 22 '22 at 21:42
  • Also, one now can call `asyncio.run()` directly instead of needing to get the `loop` and calling `loop.run_until_complete()`. – LeoRochael Jun 22 '22 at 21:42
4

Seems like there's starting to be more support for this feature since Python 3.8. In particular, look at this issue bpo-37028

If you're still on Python 3.7, maybe aiomonitor could have something that supports this feature to a certain extent.

Ivan Gozali
  • 2,089
  • 1
  • 27
  • 25