New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add IPython.embed_kernel() #1357
Conversation
This patch adds IPython.embed_kernel() as a public API. Embedding an IPython kernel in an application is useful when you want to use IPython.embed() but don't have a terminal attached on stdin and stdout. My use case is a modern gdb with Python API support #!/usr/bin/gdb --python import IPython; IPython.embed_kernel() this way I get to use ipython to explore the GDB API without the readline librarry in gdb and ipython fighting over the terminal settings. A Google search revealed that other people were interetsted in this use case as well: http://mail.scipy.org/pipermail/ipython-dev/2011-July/007928.html
Now the default behavior is to embed the Kernel into the calling application scope, which is incorrect for the basic IPython kernel. This is why the |
I'm reading up on traitlets. |
@minrk, I think I addressed the concerns raised so far. |
nice! Thanks. Pinging @fperez, who was asking about exactly this feature last week. One thing you might change from a user perspective is slightly different handling of the failed import. Right now, if pyzmq is not installed, trying IPython.embed_kernel() will raise an AttributeError. Perhaps it would be better to fallback on something with a more informative error ( |
Awesome, @scottt, many thanks! I have a long trip tomorrow so I'll be offline for a day or two, but if nobody beats me to it I'll definitely play with this and review it before the end of the week. |
@minrk, I tweaked the error message to be @fperez, Looking forward for the review :) |
from .zmq.ipkernel import embed_kernel | ||
except ImportError: | ||
def embed_kernel(*args, **kwargs): | ||
raise ImportError("IPython.embed_kernel requires pyzmq >= 2.14") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be 2.1.4, not 2.14
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about catching ImportError
here. It could mask a real problem with another import this triggers. Also, having this import here will load the ZMQ code even if it's not needed (i.e. in the plain terminal client). I think we can work round both issues by doing something like this:
def embed_kernel(*args, **kwargs):
# Lazy import
from .zmq.ipkernel import embed_kernel as real_embed_kernel
real_embed_kernel(*args, **kwargs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@takluyver, I'm sympathetic to the two issues you raised:
- Catching all ImportError's could mask the module really causing it.
- Always importing ZMQ is unnecessary for normal IPython use.
For functions that examine the local variables of their callers you can't just add another layer that wraps them though. i.e.real_embed_kernel()
would end up inspecting the locals and module globals of embed_kernel()
instead of the caller of embed_kernel()
in your snippet. I ended up changing the code like: scottt@087e9c3
if local_ns is None: | ||
local_ns = caller_locals | ||
app = IPKernelApp.instance(user_module=module, user_ns=local_ns) | ||
app.initialize() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should pass initialize an empty list here (app.initialize([])
), so that it doesn't try to parse sys.argv
, which could be problematic if called from an environment that used its own command-line args.
https://github.com/ipython/ipython/pull/1357/files#r400153 Stop embed_kernel() from parsing sys.argv since to support being called from a environments with their own command line args.
After reading #1357 (comment), I changed the code slightly in scottt@087e9c3
I also stopped catching ImportError for IPython.zmq.ipkernel. I found that IPython/zmq/init.py would catch and replace the ImportError message with "IPython.zmq requires pyzmq >= 2.1.4" which seems clear enough. Not doing the import check twice also means the minimum ZMQ version required can stay written only in one place. |
@@ -85,3 +85,10 @@ def debugx(expr,pre_msg=''): | |||
# deactivate it by uncommenting the following line, which makes it a no-op | |||
#def debugx(expr,pre_msg=''): pass | |||
|
|||
def caller_module_and_locals(): | |||
"""Returns (module, locals) of the caller""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should clarify this a bit - 'the caller' would normally mean the frame in which this is called, whereas this actually looks at the frame above that.
Possibly the function should take an optional depth argument similar to extract_vars
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@takluyver,I didn't want to add a depth
argument to ``caller_module_and_locals()` without renaming the function. See if you like scottt@a61f445
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that looks good.
def embed_kernel(module=None, local_ns=None): | ||
"""Call this to embed an IPython kernel at the current point in your program. """ | ||
(caller_module, caller_locals) = extract_module_locals_above() | ||
if module is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this logic to get the calling scope should be inside the embed_kernel
function in ipkernel
, so that it has exactly the same API if it's imported from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @takluyver here.
@scottt, this is great! One thing I'd like to have before we merge it, is at least one or two tests. Because of the kernel embedding, they might need to go into a new test group, so let us know if you need a hand. But I don't want to build too much more technical debt without new tests. In this case the test should be pretty simple to write: just start a new process with an embedded kernel that has a namespace configured with one or two variables, check those values from the test, run a piece of code in there, check the result, done. Let us know if you need a hand with the test, shouldn't be hard. |
Finally, this new capability should also be mentioned in the manual, in the embedding section. |
module = sys.modules[global_ns['__name__']] | ||
return (module, f.f_locals) | ||
|
||
def extract_module_locals_above(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function seems overly specific. Why not just replace its one use with extract_module_locals(1)
? I see that there is an extract_vars_above
function, but I don't think it's ever actually used.
Maybe I just bristle at the word above 'above'. extract_caller_module_locals
, similar to a previous patch of yours, makes more sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think caller
properly conveys what this is doing: that would imply it looks at the frame which calls this function, whereas its looking at the frame above that. above
seems less ambiguous.
I agree that the more general function is probably sufficient for the limited use of this, though.
This currently breaks all regular usage of the KernelApp, with:
Which should be fixed by adding the user_module/user_ns to the IPKernelApp as traitlets. |
@scottt, what's your take on this one? This is kind of a high-priority/value PR for us, so if you're swamped and don't think you'll be able to work on it, let us know and we'll do our best to pitch in and give you a hand. It seems there's a bit of fixing still needed before we can merge it... |
This is super close, so I'm happy to take it over the line, if you want. The only changes requested by review seem to be:
Those seem quite straightforward, and I'm happy to do them. One thing we might want to add is passing config to the embedded Kernel, changing the sig to: def embed_kernel(module=None, local_ns=None, **kwargs):
KernelApp = IPKernelApp.instance(**kwargs)
... This better matches the existing IPython.embed() |
Actually, come to think of it, |
@minrk, are you sure? I seem to recall that we need to 'prepare' a namespace that will be used by the kernel by adding a few names to it (like |
That's not how it works in the current embedded terminal, and there's no reason embed_kernel should be different. |
I certainly agree that the two should match, I just wasn't sure the embedded terminal was fully correct :) Is the |
it is not in EmbeddedShell, it is in the Kernel |
This is the second submission of this patch, revised to address concerns raised in:
#1351 (comment)
This disables the feature.
This patch adds IPython.embed_kernel() as a public API.
Embedding an IPython kernel in an application is useful when you want to
use IPython.embed() but don't have a terminal attached on stdin and stdout.
My use case is a modern gdb with Python API support
#!/usr/bin/gdb --python
import IPython; IPython.embed_kernel()
this way I get to use ipython to explore the GDB API without
the readline librarry in gdb and ipython fighting over the terminal settings.
A Google search revealed that other people were interetsted in this use
case as well:
http://mail.scipy.org/pipermail/ipython-dev/2011-July/007928.html