Repository: jupyter/jupyter_client (python)

Issue:
run_sync does not clean up the pending task after run_until_complete is interrupted
When the wrapped method (like `BlockingKernelClient.get_shell_msg`) is interrupted by KeyboardInterrupt(Ctrl+C), the event loop is interrupted, and the task ( `asyncio.Task`  ) becomes a pending state.
If I call the wrapped method again, it does not return.

I think `run_sync` should cancel the pending task.
https://stackoverflow.com/questions/30765606/whats-the-correct-way-to-clean-up-after-an-interrupted-event-loop


Pull request description:
Clean up the pending task
Fix #696 

Patch:
diff --git a/jupyter_client/utils.py b/jupyter_client/utils.py
index d410e5d..f2f3c4d 100644
--- a/jupyter_client/utils.py
+++ b/jupyter_client/utils.py
@@ -18,7 +18,12 @@ def run_sync(coro):
         import nest_asyncio  # type: ignore
 
         nest_asyncio.apply(loop)
-        return loop.run_until_complete(coro(*args, **kwargs))
+        future = asyncio.ensure_future(coro(*args, **kwargs))
+        try:
+            return loop.run_until_complete(future)
+        except BaseException as e:
+            future.cancel()
+            raise e
 
     wrapped.__doc__ = coro.__doc__
     return wrapped


Test patch:
diff --git a/jupyter_client/tests/test_utils.py b/jupyter_client/tests/test_utils.py
new file mode 100644
index 0000000..dd68491
--- /dev/null
+++ b/jupyter_client/tests/test_utils.py
@@ -0,0 +1,30 @@
+import asyncio
+from unittest import mock
+
+import pytest
+
+from jupyter_client.utils import run_sync
+
+
+@pytest.fixture
+def loop():
+    loop = asyncio.new_event_loop()
+    asyncio.set_event_loop(loop)
+    return loop
+
+
+def test_run_sync_clean_up_task(loop):
+    async def coro_never_called():
+        pytest.fail("The call to this coroutine is not expected")
+
+    # Ensure that run_sync cancels the pending task
+    with mock.patch.object(loop, "run_until_complete") as patched_loop:
+        patched_loop.side_effect = KeyboardInterrupt
+        with mock.patch("asyncio.ensure_future") as patched_ensure_future:
+            mock_future = mock.Mock()
+            patched_ensure_future.return_value = mock_future
+            with pytest.raises(KeyboardInterrupt):
+                run_sync(coro_never_called)()
+            mock_future.cancel.assert_called_once()
+            # Suppress 'coroutine ... was never awaited' warning
+            patched_ensure_future.call_args[0][0].close()