Add WIP draft `ipython` integration
This code is originally written (with much thanks) by @mikenerone:matrix.org. Adds a `tractor.trionics.ipython_embed()` which is `trio` compatible and allows straight up `await async_func()` calls in the REPL with expected default blocking semantics. More refinements to come including user config loading and eventually a foundation for what will be a console REPL + %magics for shipping work off to actor clusters and manual respawn controls and thus probably eventually obsoleting all the "parallel" stuff built into `ipython` B) Probably pertains to #130ipython_integration
parent
649c5e7504
commit
7f65d80b56
|
@ -0,0 +1,16 @@
|
|||
import trio
|
||||
from tractor.trionics import ipython_embed
|
||||
|
||||
|
||||
async def main():
|
||||
doggy = 99
|
||||
kitty = 'meow'
|
||||
|
||||
await ipython_embed()
|
||||
|
||||
assert doggy
|
||||
assert kitty
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
trio.run(main)
|
|
@ -28,6 +28,7 @@ from ._broadcast import (
|
|||
BroadcastReceiver,
|
||||
Lagged,
|
||||
)
|
||||
from ._ipython import ipython_embed
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
@ -37,4 +38,5 @@ __all__ = [
|
|||
'Lagged',
|
||||
'maybe_open_context',
|
||||
'maybe_open_nursery',
|
||||
'ipython_embed',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
# tractor: structured concurrent "actors".
|
||||
# Copyright 2018-eternity Tyler Goodlet and Mike Nerone.
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Example of running an embedded IPython shell inside an already-running
|
||||
trio loop with working autoawait (it's handy to be able to start an
|
||||
interactive REPL with your application environment fully initialized).
|
||||
This is a full solution that works around
|
||||
https://github.com/ipython/ipython/issues/680 (see
|
||||
https://gist.github.com/mikenerone/3640fdd450b4ca55ee8df4d4da5a7165 for
|
||||
how simple it *could* be). This bug should be fixed IMO (using atexit is
|
||||
a questionable design choice in the first place given the embedding
|
||||
feature of IPython IMO). As it is now, the entire IPythonAtExitContext
|
||||
context manager exists only to work around the problem, otherwise it
|
||||
would result in an error on process exit when IPython's
|
||||
atexit-registered method calls fail to save the input history. Note: You
|
||||
may wonder "Why not simply execute and unregister IPython's atexit
|
||||
registrations?" The answer is that they are bound methods, which can't
|
||||
be unregistered because you can't get a reference to the registered
|
||||
bound method (referencing the method again gives you a *new* instance of
|
||||
a bound method every time).
|
||||
|
||||
This code is credited to @mikenerone:matrix.org who put in all the hard
|
||||
work to get this integration intially up and working. Further adjustments
|
||||
to get blocking semantics on the ``await tractor.ipython_embed()`` call
|
||||
were added to the original gist:
|
||||
https://gist.github.com/mikenerone/786ce75cf8d906ae4ad1e0b57933c23f
|
||||
|
||||
'''
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import trio
|
||||
|
||||
|
||||
def trio_embedded_runner(coro):
|
||||
return trio.from_thread.run(lambda: coro)
|
||||
|
||||
|
||||
def ipython_worker(
|
||||
ns: dict[str, Any],
|
||||
ipy_done: trio.Event,
|
||||
):
|
||||
import IPython
|
||||
with IPythonAtExitContext():
|
||||
IPython.embed(using=trio_embedded_runner, user_ns=ns)
|
||||
|
||||
ipy_done.set()
|
||||
|
||||
|
||||
# TODO: get this shit workin, usage would be something like:
|
||||
# from .._ipython import ipython
|
||||
# await ipython(ns=locals())
|
||||
|
||||
|
||||
async def ipython_embed(
|
||||
ns: dict[str, Any] = {},
|
||||
nonblocking: bool = False,
|
||||
):
|
||||
# we don't require it to be installed.
|
||||
import IPython
|
||||
|
||||
# print("In trio loop")
|
||||
|
||||
# TODO: pass in the user's default config...
|
||||
# from IPython.config.loader import Config
|
||||
# cfg = Config()
|
||||
# cfg.InteractiveShellEmbed.prompt_in1="myprompt [\\#]> "
|
||||
# cfg.InteractiveShellEmbed.prompt_out="myprompt [\\#]: "
|
||||
# cfg.InteractiveShellEmbed.profile=ipythonprofile
|
||||
# directly open the shell
|
||||
# IPython.embed(config=cfg, user_ns=namespace, banner2=banner)
|
||||
|
||||
# or get shell object and open it later
|
||||
# from IPython.frontend.terminal.embed import InteractiveShellEmbed
|
||||
# shell = InteractiveShellEmbed(
|
||||
# config=cfg,
|
||||
# user_ns=namespace,
|
||||
# banner2=banner,
|
||||
# )
|
||||
# shell.user_ns = locals()
|
||||
# shell()
|
||||
|
||||
if not ns:
|
||||
ns = sys._getframe(1).f_locals
|
||||
|
||||
ipy_done = trio.Event()
|
||||
await trio.to_thread.run_sync(
|
||||
ipython_worker,
|
||||
ns,
|
||||
ipy_done,
|
||||
)
|
||||
if not nonblocking:
|
||||
await ipy_done.wait()
|
||||
|
||||
# print("Exiting trio loop")
|
||||
|
||||
|
||||
class IPythonAtExitContext:
|
||||
|
||||
ipython_modules_with_atexit = [
|
||||
"IPython.core.magics.script",
|
||||
"IPython.core.application",
|
||||
"IPython.core.interactiveshell",
|
||||
"IPython.core.history",
|
||||
"IPython.utils.io",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._calls = []
|
||||
self._patchers = []
|
||||
|
||||
def __enter__(self):
|
||||
for module in self.ipython_modules_with_atexit:
|
||||
try:
|
||||
patcher = patch(module + ".atexit", self)
|
||||
patcher.start()
|
||||
except (AttributeError, ModuleNotFoundError):
|
||||
pass
|
||||
else:
|
||||
self._patchers.append(patcher)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
for patcher in self._patchers:
|
||||
patcher.stop()
|
||||
self._patchers.clear()
|
||||
cb_exc = None
|
||||
for func, args, kwargs in self._calls:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except Exception as _exc:
|
||||
cb_exc = _exc
|
||||
self._calls.clear()
|
||||
if cb_exc and not exc_type:
|
||||
raise cb_exc
|
||||
|
||||
def register(self, func, *args, **kwargs):
|
||||
self._calls.append((func, args, kwargs))
|
||||
|
||||
def unregister(self, func):
|
||||
self._calls = [call for call in self._calls if call[0] != func]
|
Loading…
Reference in New Issue