diff --git a/IPython/__init__.py b/IPython/__init__.py index 0252940..1d7698e 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -44,10 +44,12 @@ from .config.loader import Config from .core import release from .core.application import Application from .frontend.terminal.embed import embed + from .core.error import TryNext from .core.interactiveshell import InteractiveShell from .testing import test from .utils.sysinfo import sys_info +from .utils.frame import extract_module_locals # Release data __author__ = '' @@ -55,3 +57,30 @@ for author, email in release.authors.itervalues(): __author__ += author + ' <' + email + '>\n' __license__ = release.license __version__ = release.version + +def embed_kernel(module=None, local_ns=None, **kwargs): + """Embed and start an IPython kernel in a given scope. + + Parameters + ---------- + module : ModuleType, optional + The module to load into IPython globals (default: caller) + local_ns : dict, optional + The namespace to load into IPython user namespace (default: caller) + + kwargs : various, optional + Further keyword args are relayed to the KernelApp constructor, + allowing configuration of the Kernel. Will only have an effect + on the first embed_kernel call for a given process. + + """ + + (caller_module, caller_locals) = extract_module_locals(1) + if module is None: + module = caller_module + if local_ns is None: + local_ns = caller_locals + + # Only import .zmq when we really need it + from .zmq.ipkernel import embed_kernel as real_embed_kernel + real_embed_kernel(module=module, local_ns=local_ns, **kwargs) diff --git a/IPython/utils/frame.py b/IPython/utils/frame.py index 2fb6142..13f484d 100644 --- a/IPython/utils/frame.py +++ b/IPython/utils/frame.py @@ -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 extract_module_locals(depth=0): + """Returns (module, locals) of the funciton `depth` frames away from the caller""" + f = sys._getframe(depth + 1) + global_ns = f.f_globals + module = sys.modules[global_ns['__name__']] + return (module, f.f_locals) + diff --git a/IPython/zmq/ipkernel.py b/IPython/zmq/ipkernel.py index 3164d4a..6e5f703 100755 --- a/IPython/zmq/ipkernel.py +++ b/IPython/zmq/ipkernel.py @@ -39,6 +39,7 @@ from IPython.core.shellapp import ( ) from IPython.utils import io from IPython.utils import py3compat +from IPython.utils.frame import extract_module_locals from IPython.utils.jsonutil import json_clean from IPython.utils.traitlets import ( Any, Instance, Float, Dict, CaselessStrEnum @@ -70,6 +71,17 @@ class Kernel(Configurable): iopub_socket = Instance('zmq.Socket') stdin_socket = Instance('zmq.Socket') log = Instance(logging.Logger) + + user_module = Instance('types.ModuleType') + def _user_module_changed(self, name, old, new): + if self.shell is not None: + self.shell.user_module = new + + user_ns = Dict(default_value=None) + def _user_ns_changed(self, name, old, new): + if self.shell is not None: + self.shell.user_ns = new + self.shell.init_user_ns() # Private interface @@ -110,6 +122,8 @@ class Kernel(Configurable): # Initialize the InteractiveShell subclass self.shell = ZMQInteractiveShell.instance(config=self.config, profile_dir = self.profile_dir, + user_module = self.user_module, + user_ns = self.user_ns, ) self.shell.displayhook.session = self.session self.shell.displayhook.pub_socket = self.iopub_socket @@ -571,6 +585,7 @@ class IPKernelApp(KernelApp, InteractiveShellApp): aliases = Dict(aliases) flags = Dict(flags) classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session] + # configurables pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'inline', 'auto'], config=True, @@ -649,6 +664,40 @@ def launch_kernel(*args, **kwargs): *args, **kwargs) +def embed_kernel(module=None, local_ns=None, **kwargs): + """Embed and start an IPython kernel in a given scope. + + Parameters + ---------- + module : ModuleType, optional + The module to load into IPython globals (default: caller) + local_ns : dict, optional + The namespace to load into IPython user namespace (default: caller) + + kwargs : various, optional + Further keyword args are relayed to the KernelApp constructor, + allowing configuration of the Kernel. Will only have an effect + on the first embed_kernel call for a given process. + + """ + # get the app if it exists, or set it up if it doesn't + if IPKernelApp.initialized(): + app = IPKernelApp.instance() + else: + app = IPKernelApp.instance(**kwargs) + app.initialize([]) + + # load the calling scope if not given + (caller_module, caller_locals) = extract_module_locals(1) + if module is None: + module = caller_module + if local_ns is None: + local_ns = caller_locals + + app.kernel.user_module = module + app.kernel.user_ns = local_ns + app.start() + def main(): """Run an IPKernel as an application""" app = IPKernelApp.instance() diff --git a/IPython/zmq/tests/test_embed_kernel.py b/IPython/zmq/tests/test_embed_kernel.py new file mode 100644 index 0000000..e63e9ac --- /dev/null +++ b/IPython/zmq/tests/test_embed_kernel.py @@ -0,0 +1,153 @@ +"""test IPython.embed_kernel()""" + +#------------------------------------------------------------------------------- +# Copyright (C) 2012 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#------------------------------------------------------------------------------- + +#------------------------------------------------------------------------------- +# Imports +#------------------------------------------------------------------------------- + +import os +import shutil +import sys +import tempfile +import time + +from subprocess import Popen, PIPE + +import nose.tools as nt + +from IPython.zmq.blockingkernelmanager import BlockingKernelManager +from IPython.utils import path + + +#------------------------------------------------------------------------------- +# Tests +#------------------------------------------------------------------------------- + +def setup(): + """setup temporary IPYTHONDIR for tests""" + global IPYTHONDIR + global env + global save_get_ipython_dir + + IPYTHONDIR = tempfile.mkdtemp() + env = dict(IPYTHONDIR=IPYTHONDIR) + save_get_ipython_dir = path.get_ipython_dir + path.get_ipython_dir = lambda : IPYTHONDIR + + +def teardown(): + path.get_ipython_dir = save_get_ipython_dir + + try: + shutil.rmtree(IPYTHONDIR) + except (OSError, IOError): + # no such file + pass + + +def _launch_kernel(cmd): + """start an embedded kernel in a subprocess, and wait for it to be ready + + Returns + ------- + kernel, kernel_manager: Popen instance and connected KernelManager + """ + kernel = Popen([sys.executable, '-c', cmd], stdout=PIPE, stderr=PIPE, env=env) + connection_file = os.path.join(IPYTHONDIR, + 'profile_default', + 'security', + 'kernel-%i.json' % kernel.pid + ) + # wait for connection file to exist, timeout after 5s + tic = time.time() + while not os.path.exists(connection_file) and kernel.poll() is None and time.time() < tic + 5: + time.sleep(0.1) + + if not os.path.exists(connection_file): + if kernel.poll() is None: + kernel.terminate() + raise IOError("Connection file %r never arrived" % connection_file) + + if kernel.poll() is not None: + raise IOError("Kernel failed to start") + + km = BlockingKernelManager(connection_file=connection_file) + km.load_connection_file() + km.start_channels() + + return kernel, km + +def test_embed_kernel_basic(): + """IPython.embed_kernel() is basically functional""" + cmd = '\n'.join([ + 'from IPython import embed_kernel', + 'def go():', + ' a=5', + ' b="hi there"', + ' embed_kernel()', + 'go()', + '', + ]) + + kernel, km = _launch_kernel(cmd) + shell = km.shell_channel + + # oinfo a (int) + msg_id = shell.object_info('a') + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_true(content['found']) + + msg_id = shell.execute("c=a*2") + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_equals(content['status'], u'ok') + + # oinfo c (should be 10) + msg_id = shell.object_info('c') + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_true(content['found']) + nt.assert_equals(content['string_form'], u'10') + +def test_embed_kernel_namespace(): + """IPython.embed_kernel() inherits calling namespace""" + cmd = '\n'.join([ + 'from IPython import embed_kernel', + 'def go():', + ' a=5', + ' b="hi there"', + ' embed_kernel()', + 'go()', + '', + ]) + + kernel, km = _launch_kernel(cmd) + shell = km.shell_channel + + # oinfo a (int) + msg_id = shell.object_info('a') + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_true(content['found']) + nt.assert_equals(content['string_form'], u'5') + + # oinfo b (str) + msg_id = shell.object_info('b') + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_true(content['found']) + nt.assert_equals(content['string_form'], u'hi there') + + # oinfo c (undefined) + msg_id = shell.object_info('c') + msg = shell.get_msg(block=True, timeout=2) + content = msg['content'] + nt.assert_false(content['found']) + diff --git a/docs/source/interactive/reference.txt b/docs/source/interactive/reference.txt index 88d675d..48064a2 100644 --- a/docs/source/interactive/reference.txt +++ b/docs/source/interactive/reference.txt @@ -669,6 +669,14 @@ your Python programs for this to work (detailed examples follow later):: embed() # this call anywhere in your program will start IPython +.. note:: + + As of 0.13, you can embed an IPython *kernel*, for use with qtconsole, + etc. via ``IPython.embed_kernel()`` instead of ``IPython.embed()``. + It should function just the same as regular embed, but you connect + an external frontend rather than IPython starting up in the local + terminal. + You can run embedded instances even in code which is itself being run at the IPython interactive prompt with '%run '. Since it's easy to get lost as to where you are (in your top-level IPython or in your