diff --git a/IPython/frontend/terminal/ipapp.py b/IPython/frontend/terminal/ipapp.py index 1ab9435..deea42f 100755 --- a/IPython/frontend/terminal/ipapp.py +++ b/IPython/frontend/terminal/ipapp.py @@ -217,6 +217,9 @@ class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): kernel = ("IPython.zmq.ipkernel.IPKernelApp", "Start a kernel without an attached frontend." ), + zmq=('IPython.frontend.zmqterminal.app.ZMQTerminalIPythonApp', + """Launch two-process Terminal session with 0MQ.""" + ), )) # *do* autocreate requested profile, but don't create the config file. diff --git a/IPython/frontend/zmqterminal/app.py b/IPython/frontend/zmqterminal/app.py new file mode 100644 index 0000000..eb332e2 --- /dev/null +++ b/IPython/frontend/zmqterminal/app.py @@ -0,0 +1,161 @@ +from __future__ import print_function + +import signal +import sys +import time + +from IPython.frontend.terminal.ipapp import TerminalIPythonApp + +from IPython.utils.traitlets import ( + Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any +) +from IPython.zmq.ipkernel import ( + flags as ipkernel_flags, + aliases as ipkernel_aliases, + IPKernelApp +) +from IPython.zmq.session import Session +from IPython.zmq.zmqshell import ZMQInteractiveShell +from IPython.zmq.blockingkernelmanager import BlockingKernelManager +from IPython.zmq.ipkernel import ( + flags as ipkernel_flags, + aliases as ipkernel_aliases, + IPKernelApp +) +from IPython.frontend.zmqterminal.interactiveshell import ZMQTerminalInteractiveShell + +#----------------------------------------------------------------------------- +# Network Constants +#----------------------------------------------------------------------------- + +from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS + +#----------------------------------------------------------------------------- +# Flags and Aliases +#----------------------------------------------------------------------------- + + +flags = dict(ipkernel_flags) +frontend_flags = { + 'existing' : ({'ZMQTerminalIPythonApp' : {'existing' : True}}, + "Connect to an existing kernel."), +} +flags.update(frontend_flags) +# the flags that are specific to the frontend +# these must be scrubbed before being passed to the kernel, +# or it will raise an error on unrecognized flags +frontend_flags = frontend_flags.keys() + +aliases = dict(ipkernel_aliases) + +frontend_aliases = dict( + hb = 'ZMQTerminalIPythonApp.hb_port', + shell = 'ZMQTerminalIPythonApp.shell_port', + iopub = 'ZMQTerminalIPythonApp.iopub_port', + stdin = 'ZMQTerminalIPythonApp.stdin_port', + ip = 'ZMQTerminalIPythonApp.ip', +) +aliases.update(frontend_aliases) +# also scrub aliases from the frontend +frontend_flags.extend(frontend_aliases.keys()) + +#----------------------------------------------------------------------------- +# Classes +#----------------------------------------------------------------------------- + + +class ZMQTerminalIPythonApp(TerminalIPythonApp): + """Start a terminal frontend to the IPython zmq kernel.""" + + kernel_argv = List(Unicode) + flags = Dict(flags) + aliases = Dict(aliases) + classes = List([IPKernelApp, ZMQTerminalInteractiveShell]) + + # connection info: + ip = Unicode(LOCALHOST, config=True, + help="""Set the kernel\'s IP address [default localhost]. + If the IP address is something other than localhost, then + Consoles on other machines will be able to connect + to the Kernel, so be careful!""" + ) + pure = False + hb_port = Int(0, config=True, + help="set the heartbeat port [default: random]") + shell_port = Int(0, config=True, + help="set the shell (XREP) port [default: random]") + iopub_port = Int(0, config=True, + help="set the iopub (PUB) port [default: random]") + stdin_port = Int(0, config=True, + help="set the stdin (XREQ) port [default: random]") + + existing = CBool(False, config=True, + help="Whether to connect to an already running Kernel.") + + # from qtconsoleapp: + def parse_command_line(self, argv=None): + super(ZMQTerminalIPythonApp, self).parse_command_line(argv) + if argv is None: + argv = sys.argv[1:] + + self.kernel_argv = list(argv) # copy + # kernel should inherit default config file from frontend + self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name) + # scrub frontend-specific flags + for a in argv: + + if a.startswith('-'): + key = a.lstrip('-').split('=')[0] + if key in frontend_flags: + self.kernel_argv.remove(a) + + def init_kernel_manager(self): + """init kernel manager (from qtconsole)""" + # Don't let Qt or ZMQ swallow KeyboardInterupts. + # signal.signal(signal.SIGINT, signal.SIG_DFL) + + # Create a KernelManager and start a kernel. + self.kernel_manager = BlockingKernelManager( + shell_address=(self.ip, self.shell_port), + sub_address=(self.ip, self.iopub_port), + stdin_address=(self.ip, self.stdin_port), + hb_address=(self.ip, self.hb_port), + config=self.config + ) + # start the kernel + if not self.existing: + kwargs = dict(ip=self.ip, ipython=not self.pure) + kwargs['extra_arguments'] = self.kernel_argv + self.kernel_manager.start_kernel(**kwargs) + # wait for kernel to start + time.sleep(0.5) + self.kernel_manager.start_channels() + # relay sigint to kernel + signal.signal(signal.SIGINT, self.handle_sigint) + + def init_shell(self): + self.init_kernel_manager() + self.shell = ZMQTerminalInteractiveShell.instance(config=self.config, + display_banner=False, profile_dir=self.profile_dir, + ipython_dir=self.ipython_dir, kernel_manager=self.kernel_manager) + + def handle_sigint(self, *args): + # FIXME: this doesn't work, the kernel just dies every time + self.shell.write('KeyboardInterrupt\n') + self.kernel_manager.interrupt_kernel() + + def init_code(self): + # no-op in the frontend, code gets run in the backend + pass + + +def launch_new_instance(): + """Create and run a full blown IPython instance""" + app = ZMQTerminalIPythonApp.instance() + app.initialize() + app.start() + + +if __name__ == '__main__': + launch_new_instance() + diff --git a/IPython/frontend/zmqterminal/completer.py b/IPython/frontend/zmqterminal/completer.py index 7ddeb80..817ee18 100644 --- a/IPython/frontend/zmqterminal/completer.py +++ b/IPython/frontend/zmqterminal/completer.py @@ -2,17 +2,17 @@ import readline from Queue import Empty -class ClientCompleter2p(object): +class ZMQCompleter(object): """Client-side completion machinery. How it works: self.complete will be called multiple times, with state=0,1,2,... When state=0 it should compute ALL the completion matches, and then return them for each value of state.""" - def __init__(self,client, km): + def __init__(self, shell, km): + self.shell = shell self.km = km self.matches = [] - self.client = client def complete_request(self,text): line = readline.get_line_buffer() @@ -20,15 +20,15 @@ class ClientCompleter2p(object): # send completion request to kernel # Give the kernel up to 0.5s to respond - msg_id = self.km.xreq_channel.complete(text=text, line=line, + msg_id = self.km.shell_channel.complete(text=text, line=line, cursor_pos=cursor_pos) - msg_xreq = self.km.xreq_channel.get_msg(timeout=0.5) - if msg_xreq['parent_header']['msg_id'] == msg_id: - return msg_xreq["content"]["matches"] + msg = self.km.shell_channel.get_msg(timeout=0.5) + if msg['parent_header']['msg_id'] == msg_id: + return msg["content"]["matches"] return [] - def complete(self, text, state): + def rlcomplete(self, text, state): if state == 0: try: self.matches = self.complete_request(text) @@ -39,3 +39,6 @@ class ClientCompleter2p(object): return self.matches[state] except IndexError: return None + + def complete(self, text, line, cursor_pos=None): + return self.rlcomplete(text, 0) diff --git a/IPython/frontend/zmqterminal/interactiveshell.py b/IPython/frontend/zmqterminal/interactiveshell.py new file mode 100644 index 0000000..0fd68d1 --- /dev/null +++ b/IPython/frontend/zmqterminal/interactiveshell.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +"""Frontend of ipython working with python-zmq + +Ipython's frontend, is a ipython interface that send request to kernel and proccess the kernel's outputs. + +For more details, see the ipython-zmq design +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2011 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 +#----------------------------------------------------------------------------- +from __future__ import print_function + +import bdb +import sys + +from Queue import Empty + +from IPython.core.alias import AliasManager, AliasError +from IPython.utils.warn import warn, error, fatal +from IPython.utils import io + +from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell +from IPython.frontend.zmqterminal.completer import ZMQCompleter + + +class ZMQTerminalInteractiveShell(TerminalInteractiveShell): + """A subclass of TerminalInteractiveShell that """ + + def __init__(self, *args, **kwargs): + self.km = kwargs.pop('kernel_manager') + self.session_id = self.km.session.session + super(ZMQTerminalInteractiveShell, self).__init__(*args, **kwargs) + + def init_completer(self): + """Initialize the completion machinery. + + This creates completion machinery that can be used by client code, + either interactively in-process (typically triggered by the readline + library), programatically (such as in test suites) or out-of-prcess + (typically over the network by remote frontends). + """ + from IPython.core.completerlib import (module_completer, + magic_run_completer, cd_completer) + + self.Completer = ZMQCompleter(self, self.km) + + + self.set_hook('complete_command', module_completer, str_key = 'import') + self.set_hook('complete_command', module_completer, str_key = 'from') + self.set_hook('complete_command', magic_run_completer, str_key = '%run') + self.set_hook('complete_command', cd_completer, str_key = '%cd') + + # Only configure readline if we truly are using readline. IPython can + # do tab-completion over the network, in GUIs, etc, where readline + # itself may be absent + if self.has_readline: + self.set_readline_completer() + + def run_cell(self, cell, store_history=True): + """Run a complete IPython cell. + + Parameters + ---------- + cell : str + The code (including IPython code such as %magic functions) to run. + store_history : bool + If True, the raw and translated cell will be stored in IPython's + history. For user code calling back into IPython's machinery, this + should be set to False. + """ + if (not cell) or cell.isspace(): + return + + # shell_channel.execute takes 'hidden', which is the inverse of store_hist + msg_id = self.km.shell_channel.execute(cell, not store_history) + while not self.km.shell_channel.msg_ready(): + try: + self.handle_stdin_request(timeout=0.05) + except Empty: + pass + self.handle_execute_reply(msg_id) + + #----------------- + # message handlers + #----------------- + + def handle_execute_reply(self, msg_id): + msg = self.km.shell_channel.get_msg() + if msg["parent_header"]["msg_id"] == msg_id: + if msg["content"]["status"] == 'ok' : + self.handle_iopub() + + elif msg["content"]["status"] == 'error': + for frame in msg["content"]["traceback"]: + print(frame, file=io.stderr) + + self.execution_count = msg["content"]["execution_count"] + 1 + + + def handle_iopub(self): + """ Method to procces subscribe channel's messages + + This method reads a message and processes the content in different + outputs like stdout, stderr, pyout and status + + Arguments: + sub_msg: message receive from kernel in the sub socket channel + capture by kernel manager. + """ + while self.km.sub_channel.msg_ready(): + sub_msg = self.km.sub_channel.get_msg() + msg_type = sub_msg['header']['msg_type'] + if self.session_id == sub_msg['parent_header']['session']: + if msg_type == 'status' : + if sub_msg["content"]["execution_state"] == "busy" : + pass + + elif msg_type == 'stream' : + if sub_msg["content"]["name"] == "stdout": + print(sub_msg["content"]["data"], file=io.stdout, end="") + io.stdout.flush() + elif sub_msg["content"]["name"] == "stderr" : + print(sub_msg["content"]["data"], file=io.stderr, end="") + io.stderr.flush() + + elif msg_type == 'pyout': + format_dict = sub_msg["content"]["data"] + # taken from DisplayHook.__call__: + hook = self.displayhook + hook.start_displayhook() + hook.write_output_prompt() + hook.write_format_data(format_dict) + hook.log_output(format_dict) + hook.finish_displayhook() + + def handle_stdin_request(self, timeout=0.1): + """ Method to capture raw_input + """ + msg_rep = self.km.stdin_channel.get_msg(timeout=timeout) + if self.session_id == msg_rep["parent_header"]["session"] : + raw_data = raw_input(msg_rep["content"]["prompt"]) + self.km.stdin_channel.input(raw_data) + + def mainloop(self, display_banner=False): + while True: + try: + self.interact(display_banner=display_banner) + #self.interact_with_readline() + # XXX for testing of a readline-decoupled repl loop, call + # interact_with_readline above + break + except KeyboardInterrupt: + # this should not be necessary, but KeyboardInterrupt + # handling seems rather unpredictable... + self.write("\nKeyboardInterrupt in interact()\n") + + def interact(self, display_banner=None): + """Closely emulate the interactive Python console.""" + + # batch run -> do not interact + if self.exit_now: + return + + if display_banner is None: + display_banner = self.display_banner + + if isinstance(display_banner, basestring): + self.show_banner(display_banner) + elif display_banner: + self.show_banner() + + more = False + + if self.has_readline: + self.readline_startup_hook(self.pre_readline) + # exit_now is set by a call to %Exit or %Quit, through the + # ask_exit callback. + + while not self.exit_now: + if not self.km.is_alive: + ans = self.raw_input("kernel died, restart (y/n)?") + if not ans.lower().startswith('n'): + self.km.restart_kernel(True) + else: + self.exit_now=True + continue + self.hooks.pre_prompt_hook() + if more: + try: + prompt = self.hooks.generate_prompt(True) + except: + self.showtraceback() + if self.autoindent: + self.rl_do_indent = True + + else: + try: + prompt = self.hooks.generate_prompt(False) + except: + self.showtraceback() + try: + line = self.raw_input(prompt) + if self.exit_now: + # quick exit on sys.std[in|out] close + break + if self.autoindent: + self.rl_do_indent = False + + except KeyboardInterrupt: + #double-guard against keyboardinterrupts during kbdint handling + try: + self.write('\nKeyboardInterrupt\n') + self.input_splitter.reset() + more = False + except KeyboardInterrupt: + pass + except EOFError: + if self.autoindent: + self.rl_do_indent = False + if self.has_readline: + self.readline_startup_hook(None) + self.write('\n') + self.exit() + except bdb.BdbQuit: + warn('The Python debugger has exited with a BdbQuit exception.\n' + 'Because of how pdb handles the stack, it is impossible\n' + 'for IPython to properly format this particular exception.\n' + 'IPython will resume normal operation.') + except: + # exceptions here are VERY RARE, but they can be triggered + # asynchronously by signal handlers, for example. + self.showtraceback() + else: + self.input_splitter.push(line) + more = self.input_splitter.push_accepts_more() + if (self.SyntaxTB.last_syntax_error and + self.autoedit_syntax): + self.edit_syntax_error() + if not more: + source_raw = self.input_splitter.source_reset() + self.run_cell(source_raw) + + + # Turn off the exit flag, so the mainloop can be restarted if desired + self.exit_now = False