Main question: How can I run an embedded IPython shell with a full-featured prompt (simple_prompt=False) in the namespace of a frame where an exception occurred in the context of a running IPython kernel of a Jupyter notebook?
Explanation:
I have the following usecase (which is almost solved, up to one major flaw):
In Jupyter notebook (python kernel) I run some nested function call failing_function(). Deep down in the call stack an exception happens.
I want to debug the situation on the commandline with my custom excepthook (ipydex.ips_excepthook; it opens an emebedded IPython shell right in the frame where the exception occurs and offers the possibility to move up in the frame stack, which I find more useful than the classical IPython debugger).
I can achieve this with the following steps: Within an ordinary python script:
import sys
from jupyter_console.app import ZMQTerminalIPythonApp
# mimic jupyter-console --existing: attach shell to running kernel
sys.argv = [sys.argv[0], "--existing"]
app = ZMQTerminalIPythonApp.instance()
app.initialize(None)
super(ZMQTerminalIPythonApp, app).start()
code = """
import ipydex
import traceback
try:
failing_function()
except Exception as ex:
value, tb = traceback._parse_value_tb(ex, traceback._sentinel, traceback._sentinel)
ipydex.ips_excepthook(type(ex), ex, tb)
"""
# run this code in the context of the running kernel
app.shell.run_cell(code)
Problem:
This works, but with one flaw: It only offers the simple_prompt-mode of the embedded IPython shell. Background: My custom excepthook internally creates an instance of from IPython.terminal.embed.InteractiveShellEmbed and calls its mainloop (as intended). This mainloop has two different prompt-modes: default and simple. The default mode has many desired features (colored output, TAB-completion, ...), which I desire but the simple mode does not offer. The mode is determined by this line IPython/terminal/interactiveshell.py#L134.
I tried to force the class variable InteractiveShellEmbed.simple_prompt to False but then I get RuntimeError: This event loop is already running (would have been too easy).
I see three possible ways to achieve the desired behavior:
- a) Let
InteractiveShellEmbedrun in default (i.e. not simple) mode inside the runningZMQTerminalIPythonApp. - b) Run
ZMQTerminalIPythonApp.shell.mainloop()directly in the namespace of the frame where the exception occurs (and offer the possibility to go up and down again in the frame stack). - c) Temporarily overwrite
sys.excepthookwith my customipydex.ips_excepthookand just executefailing_function()withouttry ... except.
For a): The whole story with the custom excepthook is just my motivation. The problem boils down to achieving that app.shell.run_cell('IPython.embed(colors="neutral")') runs in default (not simple) prompt mode.
For b): I dont know whether that is possible because InteractiveShellEmbed.mainloop accepts key word args local_ns and module (global name space), whereas ZMQTerminalIPythonApp.shell.mainloop does not.
For c): This works perfectly for normal scripts but not when "injecting" code in a running jupyter kernel, because overwriting sys.excepthook does not seem to have an effect.
Derived questions:
- Which of the directions (a, b, c) is the most promising?
- What would be a good next step?
- What other possibilities do I have?