Skip to content
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

Merged
merged 9 commits into from Apr 23, 2012
Merged

add IPython.embed_kernel() #1357

merged 9 commits into from Apr 23, 2012

Conversation

scottt
Copy link
Contributor

@scottt scottt commented Jan 31, 2012

This is the second submission of this patch, revised to address concerns raised in:
#1351 (comment)

  1. Allow IPython.zmq import to fail since IPython does not require zmq to exist.
    This disables the feature.
  2. Pass the namespace of the function calling embed_kernel() down as user_ns

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

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
@minrk
Copy link
Member

minrk commented Jan 31, 2012

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 embed entry point and the regular entry point in terminal IPython are not the same. One is not a subset of the other.

@scottt
Copy link
Contributor Author

scottt commented Jan 31, 2012

I'm reading up on traitlets.

@scottt
Copy link
Contributor Author

scottt commented Jan 31, 2012

@minrk, I think I addressed the concerns raised so far.

@minrk
Copy link
Member

minrk commented Jan 31, 2012

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 (ImportError("IPython.zmq requires pyzmq ≥ 2.1.4") being the most logical error to raise).

@fperez
Copy link
Member

fperez commented Jan 31, 2012

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.

@scottt
Copy link
Contributor Author

scottt commented Jan 31, 2012

@minrk, I tweaked the error message to be ImportError("IPython.embed_kernel requires pyzmq >= 2.14") as refering to IPython.embed_kernel instead of IPython.zmq seems clearer to me.
Thanks a bunch for the fast responses and pointers.

@fperez, Looking forward for the review :)
My last patch to IPython was 6+ years ago, thanks for all your work on IPython. I love this software!

from .zmq.ipkernel import embed_kernel
except ImportError:
def embed_kernel(*args, **kwargs):
raise ImportError("IPython.embed_kernel requires pyzmq >= 2.14")
Copy link
Member

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

Copy link
Member

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)

Copy link
Contributor Author

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:

  1. Catching all ImportError's could mask the module really causing it.
  2. 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()
Copy link
Member

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.
@scottt
Copy link
Contributor Author

scottt commented Mar 11, 2012

After reading #1357 (comment), I changed the code slightly in scottt@087e9c3

embed_kernel() now only imports IPython.zmq.ipkernel when called. So as not to cause unnecessary I/O and slow down IPython startup for an infrequently used feature.

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"""
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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:
Copy link
Member

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.

Copy link
Member

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.

@fperez
Copy link
Member

fperez commented Mar 13, 2012

@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.

@fperez
Copy link
Member

fperez commented Mar 13, 2012

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():
Copy link
Contributor

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.

Copy link
Member

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.

@minrk
Copy link
Member

minrk commented Apr 15, 2012

This currently breaks all regular usage of the KernelApp, with:

AttributeError: 'IPKernelApp' object has no attribute 'user_module'

Which should be fixed by adding the user_module/user_ns to the IPKernelApp as traitlets.

@ghost ghost assigned fperez Apr 15, 2012
@fperez
Copy link
Member

fperez commented Apr 16, 2012

@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...

@minrk
Copy link
Member

minrk commented Apr 18, 2012

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:

  • add user_module/ns to IPKernelApp
  • remove unnecessarily specific extract_module_locals_above()
  • add a docstring and basic test and docs

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()

@minrk
Copy link
Member

minrk commented Apr 18, 2012

Actually, come to think of it, user_module/_ns should not be part of IPKernelApp. The embedding should just set them directly on the kernel.

@fperez
Copy link
Member

fperez commented Apr 18, 2012

@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 __file__, get_ipython, etc). In that scenario, it's better to get the dict the user wants in the constructor, so we can manipulate it before the interactive loop starts.

@minrk
Copy link
Member

minrk commented Apr 19, 2012

@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 __file__, get_ipython, etc). In that scenario, it's better to get the dict the user wants in the constructor, so we can manipulate it before the interactive loop starts.

That's not how it works in the current embedded terminal, and there's no reason embed_kernel should be different.

@minrk
Copy link
Member

minrk commented Apr 19, 2012

@fperez I've got a working version here, where the user_module/ns are attached only to the Kernel, and behave essentially the same as EmbeddedShell (changing user_ns triggers init_user_ns() which does the setup).

@fperez
Copy link
Member

fperez commented Apr 19, 2012

I certainly agree that the two should match, I just wasn't sure the embedded terminal was fully correct :) Is the init_user_ns() step triggered by traitlets event handling?

@minrk
Copy link
Member

minrk commented Apr 19, 2012

it is not in EmbeddedShell, it is in the Kernel

@minrk minrk mentioned this pull request Apr 21, 2012
@fperez fperez merged commit a61f445 into ipython:master Apr 23, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants