From c515254cb4b17eb9878cabd584803b1a50e7b458 2008-08-06 01:39:48 From: Gael Varoquaux Date: 2008-08-06 01:39:48 Subject: [PATCH] First cut of subprocess execution with redirection of stdin/stdout. --- diff --git a/IPython/frontend/piped_process.py b/IPython/frontend/piped_process.py new file mode 100644 index 0000000..297ef18 --- /dev/null +++ b/IPython/frontend/piped_process.py @@ -0,0 +1,48 @@ +# encoding: utf-8 +""" +Object for encapsulating process execution by using callbacks for stdout, +stderr and stdin. +""" +__docformat__ = "restructuredtext en" + +#------------------------------------------------------------------------------- +# Copyright (C) 2008 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 subprocess import Popen, PIPE +from threading import Thread + + +class PipedProcess(Thread): + + def __init__(self, command_string, out_callback, + end_callback=None,): + self.command_string = command_string + self.out_callback = out_callback + self.end_callback = end_callback + Thread.__init__(self) + + + def run(self): + """ Start the process and hook up the callbacks. + """ + process = Popen((self.command_string + ' 2>&1', ), shell=True, + universal_newlines=True, + stdout=PIPE, stdin=PIPE) + self.process = process + while True: + out_char = process.stdout.read(1) + if out_char == '' and process.poll() is not None: + break + self.out_callback(out_char) + + if self.end_callback is not None: + self.end_callback() + + diff --git a/IPython/frontend/prefilterfrontend.py b/IPython/frontend/prefilterfrontend.py index a9b0042..9ef2089 100644 --- a/IPython/frontend/prefilterfrontend.py +++ b/IPython/frontend/prefilterfrontend.py @@ -23,26 +23,19 @@ from IPython.ipmaker import make_IPython from IPython.ipapi import IPApi from IPython.kernel.core.redirector_output_trap import RedirectorOutputTrap +from IPython.kernel.core.sync_traceback_trap import SyncTracebackTrap + +from IPython.ultraTB import ColorTB from IPython.genutils import Term import pydoc -#------------------------------------------------------------------------------- -# Utility functions (temporary, should be moved out of here) -#------------------------------------------------------------------------------- -import os -def xterm_system(command): - """ Run a command in a separate console window. - """ - os.system(("""xterm -title "%s" -e \'/bin/sh -c "%s ; """ - """echo; echo press enter to close ; """ -# """echo \\"\x1b]0;%s (finished -- press enter to close)\x07\\" ; - """read foo;"\' """) % (command, command) ) - -def system_call(command): - """ Temporary hack for aliases +def mk_system_call(system_call_function, command): + """ given a os.system replacement, and a leading string command, + returns a function that will execute the command with the given + argument string. """ def my_system_call(args): - os.system("%s %s" % (command, args)) + system_call_function("%s %s" % (command, args)) return my_system_call #------------------------------------------------------------------------------- @@ -65,14 +58,18 @@ class PrefilterFrontEnd(LineFrontEndBase): self.shell.user_global_ns = self.ipython0.user_global_ns # Make sure the raw system call doesn't get called, as we don't # have a stdin accessible. - self._ip.system = xterm_system + self._ip.system = self.system_call # XXX: Muck around with magics so that they work better # in our environment - self.ipython0.magic_ls = system_call('ls -CF') + self.ipython0.magic_ls = mk_system_call(self.system_call, + 'ls -CF') self.shell.output_trap = RedirectorOutputTrap( out_callback=self.write, err_callback=self.write, ) + self.shell.traceback_trap = SyncTracebackTrap( + formatters=[ColorTB(color_scheme='LightBG'), ] + ) # Capture and release the outputs, to make sure all the # shadow variables are set self.capture_output() @@ -115,6 +112,13 @@ class PrefilterFrontEnd(LineFrontEndBase): self.release_output() + def system_call(self, command): + """ Allows for frontend to define their own system call, to be + able capture output and redirect input. + """ + return os.system(command, args) + + def capture_output(self): """ Capture all the output mechanisms we can think of. """ diff --git a/IPython/frontend/wx/console_widget.py b/IPython/frontend/wx/console_widget.py index aab01d9..3fa3590 100644 --- a/IPython/frontend/wx/console_widget.py +++ b/IPython/frontend/wx/console_widget.py @@ -198,9 +198,11 @@ class ConsoleWidget(editwindow.EditWindow): """ Write given text to buffer, while translating the ansi escape sequences. """ - # XXX: do not put print statements in this method, the print - # statements will call this method, and you will end up with - # an infinit loop + # XXX: do not put print statements to sys.stdout/sys.stderr in + # this method, the print statements will call this method, as + # you will end up with an infinit loop + if self.debug: + print >>sys.__stderr__, text title = self.title_pat.split(text) if len(title)>1: self.title = title[-2] diff --git a/IPython/frontend/wx/wipython.py b/IPython/frontend/wx/wipython.py index 204ec61..29a65d9 100644 --- a/IPython/frontend/wx/wipython.py +++ b/IPython/frontend/wx/wipython.py @@ -5,6 +5,7 @@ ipython. import wx from wx_frontend import WxController +import __builtin__ class WIPythonController(WxController): """ Sub class of WxController that adds some application-specific @@ -19,7 +20,8 @@ class WIPythonController(WxController): def _on_key_down(self, event, skip=True): # Intercept Ctrl-D to quit if event.KeyCode == ord('D') and event.ControlDown() and \ - self.get_current_edit_buffer()=='': + self.get_current_edit_buffer()=='' and \ + not self.raw_input == __builtin__.raw_input: wx.CallAfter(self.ask_exit) else: WxController._on_key_down(self, event, skip=skip) diff --git a/IPython/frontend/wx/wx_frontend.py b/IPython/frontend/wx/wx_frontend.py index 75f6132..da346e5 100644 --- a/IPython/frontend/wx/wx_frontend.py +++ b/IPython/frontend/wx/wx_frontend.py @@ -27,7 +27,11 @@ from wx import stc from console_widget import ConsoleWidget import __builtin__ from time import sleep +import sys +from threading import Lock + +from IPython.frontend.piped_process import PipedProcess from IPython.frontend.prefilterfrontend import PrefilterFrontEnd #_COMMAND_BG = '#FAFAF1' # Nice green @@ -46,7 +50,19 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02%i\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02' debug = True - + + # Attribute to store reference to the pipes of a subprocess, if we + # are running any. + running_process = False + + # A queue for writing fast streams to the screen without flooding the + # event loop + write_buffer = [] + + # A lock to lock the write_buffer to make sure we don't empty it + # while it is being swapped + write_buffer_lock = Lock() + #-------------------------------------------------------------------------- # Public API #-------------------------------------------------------------------------- @@ -59,9 +75,6 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): ConsoleWidget.__init__(self, parent, id, pos, size, style) PrefilterFrontEnd.__init__(self) - # Capture Character keys - self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) - # Marker for running buffer. self.MarkerDefine(_RUNNING_BUFFER_MARKER, stc.STC_MARK_BACKGROUND, background=_RUNNING_BUFFER_BG) @@ -69,18 +82,28 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND, background=_ERROR_BG) + # A time for flushing the write buffer + BUFFER_FLUSH_TIMER_ID = 100 + self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID) + wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush) def do_completion(self): """ Do code completion. """ + if self.debug: + print >>sys.__stdout__, "do_completion", line = self.get_current_edit_buffer() new_line, completions = self.complete(line) if len(completions)>1: self.write_completion(completions) self.replace_current_edit_buffer(new_line) + if self.debug: + print >>sys.__stdout__, completions def do_calltip(self): + if self.debug: + print >>sys.__stdout__, "do_calltip" separators = re.compile('[\s\{\}\[\]\(\)\= ,:]') symbol = self.get_current_edit_buffer() symbol_string = separators.split(symbol)[-1] @@ -108,6 +131,8 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): """ Updates the popup completion menu if it exists. If create is true, open the menu. """ + if self.debug: + print >>sys.__stdout__, "popup_completion", line = self.get_current_edit_buffer() if (self.AutoCompActive() and not line[-1] == '.') \ or create==True: @@ -118,6 +143,8 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): residual = complete_sep.split(line)[-1] offset = len(residual) self.pop_completion(completions, offset=offset) + if self.debug: + print >>sys.__stdout__, completions def raw_input(self, prompt): @@ -148,9 +175,6 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): self.MarkerAdd(i, _RUNNING_BUFFER_MARKER) # Update the display: wx.Yield() - ## Remove the trailing "\n" for cleaner display - #self.SetSelection(self.GetLength()-1, self.GetLength()) - #self.ReplaceSelection('') self.GotoPos(self.GetLength()) PrefilterFrontEnd.execute(self, python_string, raw_string=raw_string) @@ -171,6 +195,51 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): if hasattr(self, '_cursor'): del self._cursor + + def system_call(self, command_string): + self.running_process = PipedProcess(command_string, + out_callback=self.buffered_write, + end_callback = self._end_system_call) + self.running_process.start() + # XXX: another one of these polling loops to have a blocking + # call + while self.running_process: + wx.Yield() + sleep(0.1) + + + def buffered_write(self, text): + """ A write method for streams, that caches the stream in order + to avoid flooding the event loop. + + This can be called outside of the main loop, in separate + threads. + """ + self.write_buffer_lock.acquire() + self.write_buffer.append(text) + self.write_buffer_lock.release() + if not self._buffer_flush_timer.IsRunning(): + self._buffer_flush_timer.Start(100) # milliseconds + + + def _end_system_call(self): + """ Called at the end of a system call. + """ + self.running_process = False + + + def _buffer_flush(self, event): + """ Called by the timer to flush the write buffer. + + This is always called in the mainloop, by the wx timer. + """ + self.write_buffer_lock.acquire() + write_buffer = self.write_buffer + self.write_buffer = [] + self.write_buffer_lock.release() + self.write(''.join(write_buffer)) + self._buffer_flush_timer.Stop() + def show_traceback(self): start_line = self.GetCurrentLine() @@ -184,14 +253,20 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): # Private API #-------------------------------------------------------------------------- - def _on_key_down(self, event, skip=True): """ Capture the character events, let the parent widget handle them, and put our logic afterward. """ current_line_number = self.GetCurrentLine() - # Calltips - if event.KeyCode == ord('('): + if self.running_process and event.KeyCode<256 \ + and event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): + # We are running a process, let us not be too clever. + ConsoleWidget._on_key_down(self, event, skip=skip) + if self.debug: + print >>sys.__stderr__, chr(event.KeyCode) + self.running_process.process.stdin.write(chr(event.KeyCode)) + elif event.KeyCode in (ord('('), 57): + # Calltips event.Skip() self.do_calltip() elif self.AutoCompActive(): @@ -234,7 +309,7 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): def _on_key_up(self, event, skip=True): - if event.KeyCode == 59: + if event.KeyCode in (59, ord('.')): # Intercepting '.' event.Skip() self.popup_completion(create=True) @@ -243,7 +318,6 @@ class WxController(PrefilterFrontEnd, ConsoleWidget): def _on_enter(self): if self.debug: - import sys print >>sys.__stdout__, repr(self.get_current_edit_buffer()) PrefilterFrontEnd._on_enter(self) diff --git a/IPython/kernel/core/traceback_trap.py b/IPython/kernel/core/traceback_trap.py index 5d36b36..703dd59 100644 --- a/IPython/kernel/core/traceback_trap.py +++ b/IPython/kernel/core/traceback_trap.py @@ -14,10 +14,8 @@ __docformat__ = "restructuredtext en" #------------------------------------------------------------------------------- # Imports #------------------------------------------------------------------------------- - import sys - class TracebackTrap(object): """ Object to trap and format tracebacks. """ @@ -38,9 +36,6 @@ class TracebackTrap(object): def hook(self, *args): """ This method actually implements the hook. """ - import sys - print >>sys.stderr, "I have been raised" - self.args = args def set(self):