From 5771a5f8093b4a8943e0d7d6dfb2412731e30481 2008-09-10 03:38:09 From: Brian Granger Date: 2008-09-10 03:38:09 Subject: [PATCH] Merging Gael's branch into trunk. Numerous fixes and improvements to the new wx GUI. Many of these bugs fix problems on Windows (Vista). The full IPython test suite now passes and ipythonx runs on the major platforms. Also fixed a small bug in setupegg.py. --- diff --git a/IPython/frontend/_process/killableprocess.py b/IPython/frontend/_process/killableprocess.py index e845686..955482c 100644 --- a/IPython/frontend/_process/killableprocess.py +++ b/IPython/frontend/_process/killableprocess.py @@ -50,7 +50,6 @@ import subprocess from subprocess import PIPE import sys import os -import time import types try: @@ -69,8 +68,16 @@ except ImportError: mswindows = (sys.platform == "win32") +skip = False + if mswindows: - import winprocess + import platform + if platform.uname()[3] == '' or platform.uname()[3] > '6.0.6000': + # Killable process does not work under vista when starting for + # something else than cmd. + skip = True + else: + import winprocess else: import signal @@ -78,7 +85,11 @@ if not mswindows: def DoNothing(*args): pass -class Popen(subprocess.Popen): + +if skip: + Popen = subprocess.Popen +else: + class Popen(subprocess.Popen): if not mswindows: # Override __init__ to set a preexec_fn def __init__(self, *args, **kwargs): diff --git a/IPython/frontend/_process/pipedprocess.py b/IPython/frontend/_process/pipedprocess.py index 2cda128..1b145af 100644 --- a/IPython/frontend/_process/pipedprocess.py +++ b/IPython/frontend/_process/pipedprocess.py @@ -50,7 +50,7 @@ class PipedProcess(Thread): """ env = os.environ env['TERM'] = 'xterm' - process = Popen((self.command_string + ' 2>&1', ), shell=True, + process = Popen(self.command_string + ' 2>&1', shell=True, env=env, universal_newlines=True, stdout=PIPE, stdin=PIPE, ) diff --git a/IPython/frontend/linefrontendbase.py b/IPython/frontend/linefrontendbase.py index 6f60d75..494fc71 100644 --- a/IPython/frontend/linefrontendbase.py +++ b/IPython/frontend/linefrontendbase.py @@ -20,6 +20,8 @@ import re import IPython import sys +import codeop +import traceback from frontendbase import FrontEndBase from IPython.kernel.core.interpreter import Interpreter @@ -76,6 +78,11 @@ class LineFrontEndBase(FrontEndBase): if banner is not None: self.banner = banner + + def start(self): + """ Put the frontend in a state where it is ready for user + interaction. + """ if self.banner is not None: self.write(self.banner, refresh=False) @@ -141,9 +148,18 @@ class LineFrontEndBase(FrontEndBase): and not re.findall(r"\n[\t ]*\n[\t ]*$", string)): return False else: - # Add line returns here, to make sure that the statement is - # complete. - return FrontEndBase.is_complete(self, string.rstrip() + '\n\n') + self.capture_output() + try: + # Add line returns here, to make sure that the statement is + # complete. + is_complete = codeop.compile_command(string.rstrip() + '\n\n', + "", "exec") + self.release_output() + except Exception, e: + # XXX: Hack: return True so that the + # code gets executed and the error captured. + is_complete = True + return is_complete def write(self, string, refresh=True): @@ -181,7 +197,7 @@ class LineFrontEndBase(FrontEndBase): #-------------------------------------------------------------------------- def prefilter_input(self, string): - """ Priflter the input to turn it in valid python. + """ Prefilter the input to turn it in valid python. """ string = string.replace('\r\n', '\n') string = string.replace('\t', 4*' ') @@ -210,9 +226,12 @@ class LineFrontEndBase(FrontEndBase): line = self.input_buffer new_line, completions = self.complete(line) if len(completions)>1: - self.write_completion(completions) - self.input_buffer = new_line + self.write_completion(completions, new_line=new_line) + elif not line == new_line: + self.input_buffer = new_line if self.debug: + print >>sys.__stdout__, 'line', line + print >>sys.__stdout__, 'new_line', new_line print >>sys.__stdout__, completions @@ -222,10 +241,15 @@ class LineFrontEndBase(FrontEndBase): return 80 - def write_completion(self, possibilities): + def write_completion(self, possibilities, new_line=None): """ Write the list of possible completions. + + new_line is the completed input line that should be displayed + after the completion are writen. If None, the input_buffer + before the completion is used. """ - current_buffer = self.input_buffer + if new_line is None: + new_line = self.input_buffer self.write('\n') max_len = len(max(possibilities, key=len)) + 1 @@ -246,7 +270,7 @@ class LineFrontEndBase(FrontEndBase): self.write(''.join(buf)) self.new_prompt(self.input_prompt_template.substitute( number=self.last_result['number'] + 1)) - self.input_buffer = current_buffer + self.input_buffer = new_line def new_prompt(self, prompt): @@ -275,6 +299,8 @@ class LineFrontEndBase(FrontEndBase): else: self.input_buffer += self._get_indent_string( current_buffer[:-1]) + if len(current_buffer.split('\n')) == 2: + self.input_buffer += '\t\t' if current_buffer[:-1].split('\n')[-1].rstrip().endswith(':'): self.input_buffer += '\t' diff --git a/IPython/frontend/prefilterfrontend.py b/IPython/frontend/prefilterfrontend.py index 8479132..ad6ce13 100644 --- a/IPython/frontend/prefilterfrontend.py +++ b/IPython/frontend/prefilterfrontend.py @@ -24,6 +24,7 @@ __docformat__ = "restructuredtext en" import sys from linefrontendbase import LineFrontEndBase, common_prefix +from frontendbase import FrontEndBase from IPython.ipmaker import make_IPython from IPython.ipapi import IPApi @@ -34,6 +35,7 @@ from IPython.kernel.core.sync_traceback_trap import SyncTracebackTrap from IPython.genutils import Term import pydoc import os +import sys def mk_system_call(system_call_function, command): @@ -57,6 +59,8 @@ class PrefilterFrontEnd(LineFrontEndBase): to execute the statements and the ipython0 used for code completion... """ + + debug = False def __init__(self, ipython0=None, *args, **kwargs): """ Parameters: @@ -65,12 +69,24 @@ class PrefilterFrontEnd(LineFrontEndBase): ipython0: an optional ipython0 instance to use for command prefiltering and completion. """ + LineFrontEndBase.__init__(self, *args, **kwargs) + self.shell.output_trap = RedirectorOutputTrap( + out_callback=self.write, + err_callback=self.write, + ) + self.shell.traceback_trap = SyncTracebackTrap( + formatters=self.shell.traceback_trap.formatters, + ) + + # Start the ipython0 instance: self.save_output_hooks() if ipython0 is None: # Instanciate an IPython0 interpreter to be able to use the # prefiltering. # XXX: argv=[] is a bit bold. - ipython0 = make_IPython(argv=[]) + ipython0 = make_IPython(argv=[], + user_ns=self.shell.user_ns, + user_global_ns=self.shell.user_global_ns) self.ipython0 = ipython0 # Set the pager: self.ipython0.set_hook('show_in_pager', @@ -86,24 +102,13 @@ class PrefilterFrontEnd(LineFrontEndBase): 'ls -CF') # And now clean up the mess created by ipython0 self.release_output() + + if not 'banner' in kwargs and self.banner is None: - kwargs['banner'] = self.ipython0.BANNER + """ + self.banner = self.ipython0.BANNER + """ This is the wx frontend, by Gael Varoquaux. This is EXPERIMENTAL code.""" - LineFrontEndBase.__init__(self, *args, **kwargs) - # XXX: Hack: mix the two namespaces - self.shell.user_ns.update(self.ipython0.user_ns) - self.ipython0.user_ns = self.shell.user_ns - self.shell.user_global_ns.update(self.ipython0.user_global_ns) - self.ipython0.user_global_ns = self.shell.user_global_ns - - self.shell.output_trap = RedirectorOutputTrap( - out_callback=self.write, - err_callback=self.write, - ) - self.shell.traceback_trap = SyncTracebackTrap( - formatters=self.shell.traceback_trap.formatters, - ) + self.start() #-------------------------------------------------------------------------- # FrontEndBase interface @@ -113,7 +118,7 @@ This is the wx frontend, by Gael Varoquaux. This is EXPERIMENTAL code.""" """ Use ipython0 to capture the last traceback and display it. """ self.capture_output() - self.ipython0.showtraceback() + self.ipython0.showtraceback(tb_offset=-1) self.release_output() @@ -164,6 +169,8 @@ This is the wx frontend, by Gael Varoquaux. This is EXPERIMENTAL code.""" def complete(self, line): + # FIXME: This should be factored out in the linefrontendbase + # method. word = line.split('\n')[-1].split(' ')[-1] completions = self.ipython0.complete(word) # FIXME: The proper sort should be done in the complete method. diff --git a/IPython/frontend/tests/test_process.py b/IPython/frontend/tests/test_process.py index 8f6784f..dc8db5f 100644 --- a/IPython/frontend/tests/test_process.py +++ b/IPython/frontend/tests/test_process.py @@ -20,8 +20,6 @@ from IPython.frontend._process import PipedProcess from IPython.testing import decorators as testdec -# FIXME -@testdec.skip("This doesn't work under Windows") def test_capture_out(): """ A simple test to see if we can execute a process and get the output. """ @@ -33,8 +31,6 @@ def test_capture_out(): assert result == '1' -# FIXME -@testdec.skip("This doesn't work under Windows") def test_io(): """ Checks that we can send characters on stdin to the process. """ @@ -51,8 +47,6 @@ def test_io(): assert result == test_string -# FIXME -@testdec.skip("This doesn't work under Windows") def test_kill(): """ Check that we can kill a process, and its subprocess. """ diff --git a/IPython/frontend/wx/console_widget.py b/IPython/frontend/wx/console_widget.py index 3afda8a..d61e84a 100644 --- a/IPython/frontend/wx/console_widget.py +++ b/IPython/frontend/wx/console_widget.py @@ -23,6 +23,7 @@ import wx import wx.stc as stc from wx.py import editwindow +import time import sys LINESEP = '\n' if sys.platform == 'win32': @@ -115,12 +116,15 @@ class ConsoleWidget(editwindow.EditWindow): # The color of the carret (call _apply_style() after setting) carret_color = 'BLACK' + # Store the last time a refresh was done + _last_refresh_time = 0 + #-------------------------------------------------------------------------- # Public API #-------------------------------------------------------------------------- def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, - size=wx.DefaultSize, style=0, ): + size=wx.DefaultSize, style=wx.WANTS_CHARS, ): editwindow.EditWindow.__init__(self, parent, id, pos, size, style) self._configure_scintilla() @@ -168,9 +172,14 @@ class ConsoleWidget(editwindow.EditWindow): self.GotoPos(self.GetLength()) if refresh: - # Maybe this is faster than wx.Yield() - self.ProcessEvent(wx.PaintEvent()) - #wx.Yield() + current_time = time.time() + if current_time - self._last_refresh_time > 0.03: + if sys.platform == 'win32': + wx.SafeYield() + else: + wx.Yield() + # self.ProcessEvent(wx.PaintEvent()) + self._last_refresh_time = current_time def new_prompt(self, prompt): @@ -183,7 +192,6 @@ class ConsoleWidget(editwindow.EditWindow): # now we update our cursor giving end of prompt self.current_prompt_pos = self.GetLength() self.current_prompt_line = self.GetCurrentLine() - wx.Yield() self.EnsureCaretVisible() diff --git a/IPython/frontend/wx/wx_frontend.py b/IPython/frontend/wx/wx_frontend.py index b695897..d4182cc 100644 --- a/IPython/frontend/wx/wx_frontend.py +++ b/IPython/frontend/wx/wx_frontend.py @@ -128,6 +128,7 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): # while it is being swapped _out_buffer_lock = Lock() + # The different line markers used to higlight the prompts. _markers = dict() #-------------------------------------------------------------------------- @@ -135,12 +136,16 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): #-------------------------------------------------------------------------- def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, - size=wx.DefaultSize, style=wx.CLIP_CHILDREN, + size=wx.DefaultSize, + style=wx.CLIP_CHILDREN|wx.WANTS_CHARS, *args, **kwds): """ Create Shell instance. """ ConsoleWidget.__init__(self, parent, id, pos, size, style) PrefilterFrontEnd.__init__(self, **kwds) + + # Stick in our own raw_input: + self.ipython0.raw_input = self.raw_input # Marker for complete buffer. self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND, @@ -164,9 +169,11 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): # Inject self in namespace, for debug if self.debug: self.shell.user_ns['self'] = self + # Inject our own raw_input in namespace + self.shell.user_ns['raw_input'] = self.raw_input - def raw_input(self, prompt): + def raw_input(self, prompt=''): """ A replacement from python's raw_input. """ self.new_prompt(prompt) @@ -174,15 +181,13 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): if hasattr(self, '_cursor'): del self._cursor self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS)) - self.waiting = True self.__old_on_enter = self._on_enter + event_loop = wx.EventLoop() def my_on_enter(): - self.waiting = False + event_loop.Exit() self._on_enter = my_on_enter - # XXX: Busy waiting, ugly. - while self.waiting: - wx.Yield() - sleep(0.1) + # XXX: Running a separate event_loop. Ugly. + event_loop.Run() self._on_enter = self.__old_on_enter self._input_state = 'buffering' self._cursor = wx.BusyCursor() @@ -191,16 +196,18 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): def system_call(self, command_string): self._input_state = 'subprocess' + event_loop = wx.EventLoop() + def _end_system_call(): + self._input_state = 'buffering' + self._running_process = False + event_loop.Exit() + self._running_process = PipedProcess(command_string, out_callback=self.buffered_write, - end_callback = self._end_system_call) + end_callback = _end_system_call) self._running_process.start() - # XXX: another one of these polling loops to have a blocking - # call - wx.Yield() - while self._running_process: - wx.Yield() - sleep(0.1) + # XXX: Running a separate event_loop. Ugly. + event_loop.Run() # Be sure to flush the buffer. self._buffer_flush(event=None) @@ -226,8 +233,9 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): for name in symbol_string.split('.')[1:] + ['__doc__']: symbol = getattr(symbol, name) self.AutoCompCancel() - wx.Yield() - self.CallTipShow(self.GetCurrentPos(), symbol) + # Check that the symbol can indeed be converted to a string: + symbol += '' + wx.CallAfter(self.CallTipShow, self.GetCurrentPos(), symbol) except: # The retrieve symbol couldn't be converted to a string pass @@ -238,9 +246,9 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): true, open the menu. """ if self.debug: - print >>sys.__stdout__, "_popup_completion", + print >>sys.__stdout__, "_popup_completion" line = self.input_buffer - if (self.AutoCompActive() and not line[-1] == '.') \ + if (self.AutoCompActive() and line and not line[-1] == '.') \ or create==True: suggestion, completions = self.complete(line) offset=0 @@ -284,19 +292,21 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): if i in self._markers: self.MarkerDeleteHandle(self._markers[i]) self._markers[i] = self.MarkerAdd(i, _COMPLETE_BUFFER_MARKER) - # Update the display: - wx.Yield() - self.GotoPos(self.GetLength()) - PrefilterFrontEnd.execute(self, python_string, raw_string=raw_string) + # Use a callafter to update the display robustly under windows + def callback(): + self.GotoPos(self.GetLength()) + PrefilterFrontEnd.execute(self, python_string, + raw_string=raw_string) + wx.CallAfter(callback) def save_output_hooks(self): self.__old_raw_input = __builtin__.raw_input PrefilterFrontEnd.save_output_hooks(self) def capture_output(self): - __builtin__.raw_input = self.raw_input self.SetLexer(stc.STC_LEX_NULL) PrefilterFrontEnd.capture_output(self) + __builtin__.raw_input = self.raw_input def release_output(self): @@ -316,12 +326,24 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): def show_traceback(self): start_line = self.GetCurrentLine() PrefilterFrontEnd.show_traceback(self) - wx.Yield() + self.ProcessEvent(wx.PaintEvent()) + #wx.Yield() for i in range(start_line, self.GetCurrentLine()): self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER) #-------------------------------------------------------------------------- + # FrontEndBase interface + #-------------------------------------------------------------------------- + + def render_error(self, e): + start_line = self.GetCurrentLine() + self.write('\n' + e + '\n') + for i in range(start_line, self.GetCurrentLine()): + self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER) + + + #-------------------------------------------------------------------------- # ConsoleWidget interface #-------------------------------------------------------------------------- @@ -351,7 +373,8 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): if self._input_state == 'subprocess': if self.debug: print >>sys.__stderr__, 'Killing running process' - self._running_process.process.kill() + if hasattr(self._running_process, 'process'): + self._running_process.process.kill() elif self._input_state == 'buffering': if self.debug: print >>sys.__stderr__, 'Raising KeyboardInterrupt' @@ -376,7 +399,7 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): char = '\04' self._running_process.process.stdin.write(char) self._running_process.process.stdin.flush() - elif event.KeyCode in (ord('('), 57): + elif event.KeyCode in (ord('('), 57, 53): # Calltips event.Skip() self.do_calltip() @@ -410,8 +433,8 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): self.input_buffer = new_buffer # Tab-completion elif event.KeyCode == ord('\t'): - last_line = self.input_buffer.split('\n')[-1] - if not re.match(r'^\s*$', last_line): + current_line, current_line_number = self.CurLine + if not re.match(r'^\s*$', current_line): self.complete_current_input() if self.AutoCompActive(): wx.CallAfter(self._popup_completion, create=True) @@ -427,7 +450,7 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): if event.KeyCode in (59, ord('.')): # Intercepting '.' event.Skip() - self._popup_completion(create=True) + wx.CallAfter(self._popup_completion, create=True) else: ConsoleWidget._on_key_up(self, event, skip=skip) @@ -456,13 +479,6 @@ class WxController(ConsoleWidget, PrefilterFrontEnd): # Private API #-------------------------------------------------------------------------- - def _end_system_call(self): - """ Called at the end of a system call. - """ - self._input_state = 'buffering' - self._running_process = False - - def _buffer_flush(self, event): """ Called by the timer to flush the write buffer. diff --git a/IPython/kernel/core/interpreter.py b/IPython/kernel/core/interpreter.py index f7d906c..bca170b 100644 --- a/IPython/kernel/core/interpreter.py +++ b/IPython/kernel/core/interpreter.py @@ -680,6 +680,13 @@ class Interpreter(object): # how trailing whitespace is handled, but this seems to work. python = python.strip() + # The compiler module does not like unicode. We need to convert + # it encode it: + if isinstance(python, unicode): + # Use the utf-8-sig BOM so the compiler detects this a UTF-8 + # encode string. + python = '\xef\xbb\xbf' + python.encode('utf-8') + # The compiler module will parse the code into an abstract syntax tree. ast = compiler.parse(python) diff --git a/IPython/kernel/core/tests/test_interpreter.py b/IPython/kernel/core/tests/test_interpreter.py new file mode 100644 index 0000000..3efc4f2 --- /dev/null +++ b/IPython/kernel/core/tests/test_interpreter.py @@ -0,0 +1,26 @@ +# encoding: utf-8 + +"""This file contains unittests for the interpreter.py module.""" + +__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 IPython.kernel.core.interpreter import Interpreter + +def test_unicode(): + """ Test unicode handling with the interpreter. + """ + i = Interpreter() + i.execute_python(u'print "ù"') + i.execute_python('print "ù"') + diff --git a/IPython/testing/plugin/Makefile b/IPython/testing/plugin/Makefile index 878946c..9d8157a 100644 --- a/IPython/testing/plugin/Makefile +++ b/IPython/testing/plugin/Makefile @@ -1,6 +1,5 @@ # Set this prefix to where you want to install the plugin -PREFIX=~/usr/local -PREFIX=~/tmp/local +PREFIX=/usr/local NOSE0=nosetests -vs --with-doctest --doctest-tests --detailed-errors NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt \ diff --git a/setupegg.py b/setupegg.py index c59ea94..0924b19 100755 --- a/setupegg.py +++ b/setupegg.py @@ -1,14 +1,8 @@ #!/usr/bin/env python """Wrapper to run setup.py using setuptools.""" -import os import sys -# Add my local path to sys.path -home = os.environ['HOME'] -sys.path.insert(0,'%s/usr/local/lib/python%s/site-packages' % - (home,sys.version[:3])) - # now, import setuptools and call the actual setup import setuptools # print sys.argv