ipython icon indicating copy to clipboard operation
ipython copied to clipboard

KeyboardInterrupt on awaited async function does not stop it completely

Open MarcusZuber opened this issue 1 year ago • 0 comments

This seam to be related to https://github.com/ipython/ipython/issues/13737 but in a different nesting order of blocking/async code.

When running an async function with await directly in ipython and calling a KeybordInterrupt it does not completely stop it (it seams that the keyboard interrupt is sent to the currently running function but not to all levels). When executing the async function with asyncio.run() it stops it correctly.

Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.17.2 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %gui asyncio
Installed asyncio event loop hook.

In [2]: import asyncio

In [3]: async def run_func():
   ...:     for i in range(20):
   ...:         print("foo")
   ...:         await asyncio.sleep(2)
   ...: 

In [4]: await run_func()
foo
foo
^C---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
File ~/.local/lib/python3.10/site-packages/IPython/core/async_helpers.py:56, in _AsyncIORunner.__call__(self, coro)
     52 def __call__(self, coro):
     53     """
     54     Handler for asyncio autoawait
     55     """
---> 56     return get_asyncio_loop().run_until_complete(coro)

File /usr/lib/python3.10/asyncio/base_events.py:636, in BaseEventLoop.run_until_complete(self, future)
    634 future.add_done_callback(_run_until_complete_cb)
    635 try:
--> 636     self.run_forever()
    637 except:
    638     if new_task and future.done() and not future.cancelled():
    639         # The coroutine raised a BaseException. Consume the exception
    640         # to not log a warning, the caller doesn't have access to the
    641         # local task.

File /usr/lib/python3.10/asyncio/base_events.py:603, in BaseEventLoop.run_forever(self)
    601 events._set_running_loop(self)
    602 while True:
--> 603     self._run_once()
    604     if self._stopping:
    605         break

File /usr/lib/python3.10/asyncio/base_events.py:1871, in BaseEventLoop._run_once(self)
   1868     when = self._scheduled[0]._when
   1869     timeout = min(max(0, when - self.time()), MAXIMUM_SELECT_TIMEOUT)
-> 1871 event_list = self._selector.select(timeout)
   1872 self._process_events(event_list)
   1873 # Needed to break cycles when an exception occurs.

File /usr/lib/python3.10/selectors.py:469, in EpollSelector.select(self, timeout)
    467 ready = []
    468 try:
--> 469     fd_event_list = self._selector.poll(timeout, max_ev)
    470 except InterruptedError:
    471     return ready

KeyboardInterrupt: 

foo # From here I have my prompt back but the loop is still running in the background
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo # Now the loop finished
In [4]: asyncio.run(run_func())
foo
foo
^C---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[5], line 1
----> 1 asyncio.run(run_func())

File /usr/lib/python3.10/asyncio/runners.py:44, in run(main, debug)
     42     if debug is not None:
     43         loop.set_debug(debug)
---> 44     return loop.run_until_complete(main)
     45 finally:
     46     try:

File /usr/lib/python3.10/asyncio/base_events.py:636, in BaseEventLoop.run_until_complete(self, future)
    634 future.add_done_callback(_run_until_complete_cb)
    635 try:
--> 636     self.run_forever()
    637 except:
    638     if new_task and future.done() and not future.cancelled():
    639         # The coroutine raised a BaseException. Consume the exception
    640         # to not log a warning, the caller doesn't have access to the
    641         # local task.

File /usr/lib/python3.10/asyncio/base_events.py:603, in BaseEventLoop.run_forever(self)
    601 events._set_running_loop(self)
    602 while True:
--> 603     self._run_once()
    604     if self._stopping:
    605         break

File /usr/lib/python3.10/asyncio/base_events.py:1871, in BaseEventLoop._run_once(self)
   1868     when = self._scheduled[0]._when
   1869     timeout = min(max(0, when - self.time()), MAXIMUM_SELECT_TIMEOUT)
-> 1871 event_list = self._selector.select(timeout)
   1872 self._process_events(event_list)
   1873 # Needed to break cycles when an exception occurs.

File /usr/lib/python3.10/selectors.py:469, in EpollSelector.select(self, timeout)
    467 ready = []
    468 try:
--> 469     fd_event_list = self._selector.poll(timeout, max_ev)
    470 except InterruptedError:
    471     return ready

KeyboardInterrupt: 

In [6]:  # No more printings

Present in:

python3 -c "import IPython; print(IPython.sys_info())"
{'commit_hash': 'ef3bae307',
 'commit_source': 'installation',
 'default_encoding': 'utf-8',
 'ipython_path': '/home/username/.local/lib/python3.10/site-packages/IPython',
 'ipython_version': '8.17.2',
 'os_name': 'posix',
 'platform': 'Linux-6.2.0-36-generic-x86_64-with-glibc2.35',
 'sys_executable': '/usr/bin/python3',
 'sys_platform': 'linux',
 'sys_version': '3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]'}
{'commit_hash': 'b5cd02544',
 'commit_source': 'installation',
 'default_encoding': 'utf-8',
 'ipython_path': '/usr/local/lib/python3.8/dist-packages/IPython',
 'ipython_version': '8.2.0',
 'os_name': 'posix',
 'platform': 'Linux-5.4.0-164-generic-x86_64-with-glibc2.29',
 'sys_executable': '/usr/bin/python3',
 'sys_platform': 'linux',
 'sys_version': '3.8.10 (default, May 26 2023, 14:05:08) \n[GCC 9.4.0]'}

MarcusZuber avatar Nov 21 '23 09:11 MarcusZuber