diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index d683e8263..75d4d5e64 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -72,6 +72,10 @@ {'IPKernelApp' : {'pylab' : 'auto'}}, """Pre-load matplotlib and numpy for interactive use with the default matplotlib backend."""), + 'trio-loop' : ( + {'InteractiveShell' : {'trio_loop' : False}}, + 'Enable Trio as main event loop.' + ), }) # inherit flags&aliases for any IPython shell apps @@ -147,6 +151,7 @@ def abs_connection_file(self): # streams, etc. no_stdout = Bool(False, help="redirect stdout to the null device").tag(config=True) no_stderr = Bool(False, help="redirect stderr to the null device").tag(config=True) + trio_loop = Bool(False, help="Set main event loop.").tag(config=True) quiet = Bool(True, help="Only send stdout/stderr to output stream").tag(config=True) outstream_class = DottedObjectName('ipykernel.iostream.OutStream', help="The importstring for the OutStream factory").tag(config=True) @@ -579,10 +584,19 @@ def start(self): self.poller.start() self.kernel.start() self.io_loop = ioloop.IOLoop.current() - try: - self.io_loop.start() - except KeyboardInterrupt: - pass + if self.trio_loop: + from ipykernel.trio_runner import TrioRunner + tr = TrioRunner() + tr.initialize(self.kernel, self.io_loop) + try: + tr.run() + except KeyboardInterrupt: + pass + else: + try: + self.io_loop.start() + except KeyboardInterrupt: + pass launch_new_instance = IPKernelApp.launch_instance diff --git a/ipykernel/trio_runner.py b/ipykernel/trio_runner.py new file mode 100644 index 000000000..4ba1da196 --- /dev/null +++ b/ipykernel/trio_runner.py @@ -0,0 +1,60 @@ +import builtins +import logging +import signal +import threading +import traceback +import warnings + +import trio + + +class TrioRunner: + def __init__(self): + self._cell_cancel_scope = None + self._trio_token = None + + def initialize(self, kernel, io_loop): + kernel.shell.set_trio_runner(self) + kernel.shell.run_line_magic('autoawait', 'trio') + kernel.shell.magics_manager.magics['line']['autoawait'] = \ + lambda _: warnings.warn("Autoawait isn't allowed in Trio " + "background loop mode.") + bg_thread = threading.Thread(target=io_loop.start, daemon=True, + name='TornadoBackground') + bg_thread.start() + + def interrupt(self, signum, frame): + if self._cell_cancel_scope: + self._cell_cancel_scope.cancel() + else: + raise Exception('Kernel interrupted but no cell is running') + + def run(self): + old_sig = signal.signal(signal.SIGINT, self.interrupt) + + def log_nursery_exc(exc): + exc = '\n'.join(traceback.format_exception(type(exc), exc, + exc.__traceback__)) + logging.error('An exception occurred in a global nursery task.\n%s', + exc) + + async def trio_main(): + self._trio_token = trio.hazmat.current_trio_token() + async with trio.open_nursery() as nursery: + # TODO This hack prevents the nursery from cancelling all child + # tasks when an uncaught exception occurs, but it's ugly. + nursery._add_exc = log_nursery_exc + builtins.GLOBAL_NURSERY = nursery + await trio.sleep_forever() + + trio.run(trio_main) + signal.signal(signal.SIGINT, old_sig) + + def __call__(self, async_fn): + async def loc(coro): + self._cell_cancel_scope = trio.CancelScope() + with self._cell_cancel_scope: + return await coro + self._cell_cancel_scope = None + + return trio.from_thread.run(loc, async_fn, trio_token=self._trio_token)