diff --git a/IPython/Extensions/ipy_legacy.py b/IPython/Extensions/ipy_legacy.py index e59a5db..2807dfd 100644 --- a/IPython/Extensions/ipy_legacy.py +++ b/IPython/Extensions/ipy_legacy.py @@ -49,7 +49,7 @@ ip.expose_magic("rehash", magic_rehash) def magic_Quit(self, parameter_s=''): """Exit IPython without confirmation (like %Exit).""" - self.shell.exit_now = True + self.shell.ask_exit() ip.expose_magic("Quit", magic_Quit) diff --git a/IPython/Magic.py b/IPython/Magic.py index f416884..ab1a2f9 100644 --- a/IPython/Magic.py +++ b/IPython/Magic.py @@ -2483,7 +2483,7 @@ Defaulting color scheme to 'NoColor'""" def magic_Exit(self, parameter_s=''): """Exit IPython without confirmation.""" - self.shell.exit_now = True + self.shell.ask_exit() #...................................................................... # Functions to implement unix shell-type things diff --git a/IPython/frontend/_process/__init__.py b/IPython/frontend/_process/__init__.py new file mode 100644 index 0000000..af9be72 --- /dev/null +++ b/IPython/frontend/_process/__init__.py @@ -0,0 +1,19 @@ +""" +Package for dealing for process execution in a callback environment, in a +portable way. + +killable_process.py is a wrapper of subprocess.Popen that allows the +subprocess and its children to be killed in a reliable way, including +under windows. + +winprocess.py is required by killable_process.py to kill processes under +windows. + +piped_process.py wraps process execution with callbacks to print output, +in a non-blocking way. It can be used to interact with a subprocess in eg +a GUI event loop. +""" + +from pipedprocess import PipedProcess + + diff --git a/IPython/frontend/_process/killableprocess.py b/IPython/frontend/_process/killableprocess.py new file mode 100644 index 0000000..e845686 --- /dev/null +++ b/IPython/frontend/_process/killableprocess.py @@ -0,0 +1,168 @@ +# Addapted from killableprocess.py. +#______________________________________________________________________________ +# +# killableprocess - subprocesses which can be reliably killed +# +# Parts of this module are copied from the subprocess.py file contained +# in the Python distribution. +# +# Copyright (c) 2003-2004 by Peter Astrand +# +# Additions and modifications written by Benjamin Smedberg +# are Copyright (c) 2006 by the Mozilla Foundation +# +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +r"""killableprocess - Subprocesses which can be reliably killed + +This module is a subclass of the builtin "subprocess" module. It allows +processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. + +It also adds a timeout argument to Wait() for a limited period of time before +forcefully killing the process. + +Note: On Windows, this module requires Windows 2000 or higher (no support for +Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with +Python 2.5+ or available from http://python.net/crew/theller/ctypes/ +""" + +import subprocess +from subprocess import PIPE +import sys +import os +import time +import types + +try: + from subprocess import CalledProcessError +except ImportError: + # Python 2.4 doesn't implement CalledProcessError + class CalledProcessError(Exception): + """This exception is raised when a process run by check_call() returns + a non-zero exit status. The exit status will be stored in the + returncode attribute.""" + def __init__(self, returncode, cmd): + self.returncode = returncode + self.cmd = cmd + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +mswindows = (sys.platform == "win32") + +if mswindows: + import winprocess +else: + import signal + +if not mswindows: + def DoNothing(*args): + pass + +class Popen(subprocess.Popen): + if not mswindows: + # Override __init__ to set a preexec_fn + def __init__(self, *args, **kwargs): + if len(args) >= 7: + raise Exception("Arguments preexec_fn and after must be passed by keyword.") + + real_preexec_fn = kwargs.pop("preexec_fn", None) + def setpgid_preexec_fn(): + os.setpgid(0, 0) + if real_preexec_fn: + apply(real_preexec_fn) + + kwargs['preexec_fn'] = setpgid_preexec_fn + + subprocess.Popen.__init__(self, *args, **kwargs) + + if mswindows: + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + if not isinstance(args, types.StringTypes): + args = subprocess.list2cmdline(args) + + if startupinfo is None: + startupinfo = winprocess.STARTUPINFO() + + if None not in (p2cread, c2pwrite, errwrite): + startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES + + startupinfo.hStdInput = int(p2cread) + startupinfo.hStdOutput = int(c2pwrite) + startupinfo.hStdError = int(errwrite) + if shell: + startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = winprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", "cmd.exe") + args = comspec + " /c " + args + + # We create a new job for this process, so that we can kill + # the process and any sub-processes + self._job = winprocess.CreateJobObject() + + creationflags |= winprocess.CREATE_SUSPENDED + creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT + + hp, ht, pid, tid = winprocess.CreateProcess( + executable, args, + None, None, # No special security + 1, # Must inherit handles! + creationflags, + winprocess.EnvironmentBlock(env), + cwd, startupinfo) + + self._child_created = True + self._handle = hp + self._thread = ht + self.pid = pid + + winprocess.AssignProcessToJobObject(self._job, hp) + winprocess.ResumeThread(ht) + + if p2cread is not None: + p2cread.Close() + if c2pwrite is not None: + c2pwrite.Close() + if errwrite is not None: + errwrite.Close() + + def kill(self, group=True): + """Kill the process. If group=True, all sub-processes will also be killed.""" + if mswindows: + if group: + winprocess.TerminateJobObject(self._job, 127) + else: + winprocess.TerminateProcess(self._handle, 127) + self.returncode = 127 + else: + if group: + os.killpg(self.pid, signal.SIGKILL) + else: + os.kill(self.pid, signal.SIGKILL) + self.returncode = -9 + + diff --git a/IPython/frontend/_process/pipedprocess.py b/IPython/frontend/_process/pipedprocess.py new file mode 100644 index 0000000..2cda128 --- /dev/null +++ b/IPython/frontend/_process/pipedprocess.py @@ -0,0 +1,74 @@ +# 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 killableprocess import Popen, PIPE +from threading import Thread +from time import sleep +import os + +class PipedProcess(Thread): + """ Class that encapsulates process execution by using callbacks for + stdout, stderr and stdin, and providing a reliable way of + killing it. + """ + + def __init__(self, command_string, out_callback, + end_callback=None,): + """ command_string: the command line executed to start the + process. + + out_callback: the python callable called on stdout/stderr. + + end_callback: an optional callable called when the process + finishes. + + These callbacks are called from a different thread as the + thread from which is started. + """ + 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. + """ + env = os.environ + env['TERM'] = 'xterm' + process = Popen((self.command_string + ' 2>&1', ), shell=True, + env=env, + universal_newlines=True, + stdout=PIPE, stdin=PIPE, ) + self.process = process + while True: + out_char = process.stdout.read(1) + if out_char == '': + if process.poll() is not None: + # The process has finished + break + else: + # The process is not giving any interesting + # output. No use polling it immediatly. + sleep(0.1) + else: + self.out_callback(out_char) + + if self.end_callback is not None: + self.end_callback() + + diff --git a/IPython/frontend/_process/winprocess.py b/IPython/frontend/_process/winprocess.py new file mode 100644 index 0000000..9114fcf --- /dev/null +++ b/IPython/frontend/_process/winprocess.py @@ -0,0 +1,264 @@ +# A module to expose various thread/process/job related structures and +# methods from kernel32 +# +# The MIT License +# +# Copyright (c) 2006 the Mozilla Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD + +LPVOID = c_void_p +LPBYTE = POINTER(BYTE) +LPDWORD = POINTER(DWORD) + +SW_HIDE = 0 + +def ErrCheckBool(result, func, args): + """errcheck function for Windows functions that return a BOOL True + on success""" + if not result: + raise WinError() + return args + +# CloseHandle() + +CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) +CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) +CloseHandle.errcheck = ErrCheckBool + +# AutoHANDLE + +class AutoHANDLE(HANDLE): + """Subclass of HANDLE which will call CloseHandle() on deletion.""" + def Close(self): + if self.value: + CloseHandle(self) + self.value = 0 + + def __del__(self): + self.Close() + + def __int__(self): + return self.value + +def ErrCheckHandle(result, func, args): + """errcheck function for Windows functions that return a HANDLE.""" + if not result: + raise WinError() + return AutoHANDLE(result) + +# PROCESS_INFORMATION structure + +class PROCESS_INFORMATION(Structure): + _fields_ = [("hProcess", HANDLE), + ("hThread", HANDLE), + ("dwProcessID", DWORD), + ("dwThreadID", DWORD)] + + def __init__(self): + Structure.__init__(self) + + self.cb = sizeof(self) + +LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) + +# STARTUPINFO structure + +class STARTUPINFO(Structure): + _fields_ = [("cb", DWORD), + ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), + ("lpTitle", LPWSTR), + ("dwX", DWORD), + ("dwY", DWORD), + ("dwXSize", DWORD), + ("dwYSize", DWORD), + ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), + ("dwFillAttribute", DWORD), + ("dwFlags", DWORD), + ("wShowWindow", WORD), + ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), + ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), + ("hStdError", HANDLE) + ] +LPSTARTUPINFO = POINTER(STARTUPINFO) + +STARTF_USESHOWWINDOW = 0x01 +STARTF_USESIZE = 0x02 +STARTF_USEPOSITION = 0x04 +STARTF_USECOUNTCHARS = 0x08 +STARTF_USEFILLATTRIBUTE = 0x10 +STARTF_RUNFULLSCREEN = 0x20 +STARTF_FORCEONFEEDBACK = 0x40 +STARTF_FORCEOFFFEEDBACK = 0x80 +STARTF_USESTDHANDLES = 0x100 + +# EnvironmentBlock + +class EnvironmentBlock: + """An object which can be passed as the lpEnv parameter of CreateProcess. + It is initialized with a dictionary.""" + + def __init__(self, dict): + if not dict: + self._as_parameter_ = None + else: + values = ["%s=%s" % (key, value) + for (key, value) in dict.iteritems()] + values.append("") + self._as_parameter_ = LPCWSTR("\0".join(values)) + +# CreateProcess() + +CreateProcessProto = WINFUNCTYPE(BOOL, # Return type + LPCWSTR, # lpApplicationName + LPWSTR, # lpCommandLine + LPVOID, # lpProcessAttributes + LPVOID, # lpThreadAttributes + BOOL, # bInheritHandles + DWORD, # dwCreationFlags + LPVOID, # lpEnvironment + LPCWSTR, # lpCurrentDirectory + LPSTARTUPINFO, # lpStartupInfo + LPPROCESS_INFORMATION # lpProcessInformation + ) + +CreateProcessFlags = ((1, "lpApplicationName", None), + (1, "lpCommandLine"), + (1, "lpProcessAttributes", None), + (1, "lpThreadAttributes", None), + (1, "bInheritHandles", True), + (1, "dwCreationFlags", 0), + (1, "lpEnvironment", None), + (1, "lpCurrentDirectory", None), + (1, "lpStartupInfo"), + (2, "lpProcessInformation")) + +def ErrCheckCreateProcess(result, func, args): + ErrCheckBool(result, func, args) + # return a tuple (hProcess, hThread, dwProcessID, dwThreadID) + pi = args[9] + return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID + +CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32), + CreateProcessFlags) +CreateProcess.errcheck = ErrCheckCreateProcess + +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NEW_CONSOLE = 0x00000010 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_NO_WINDOW = 0x08000000 +CREATE_SUSPENDED = 0x00000004 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +DEBUG_PROCESS = 0x00000001 +DETACHED_PROCESS = 0x00000008 + +# CreateJobObject() + +CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type + LPVOID, # lpJobAttributes + LPCWSTR # lpName + ) + +CreateJobObjectFlags = ((1, "lpJobAttributes", None), + (1, "lpName", None)) + +CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), + CreateJobObjectFlags) +CreateJobObject.errcheck = ErrCheckHandle + +# AssignProcessToJobObject() + +AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + HANDLE # hProcess + ) +AssignProcessToJobObjectFlags = ((1, "hJob"), + (1, "hProcess")) +AssignProcessToJobObject = AssignProcessToJobObjectProto( + ("AssignProcessToJobObject", windll.kernel32), + AssignProcessToJobObjectFlags) +AssignProcessToJobObject.errcheck = ErrCheckBool + +# ResumeThread() + +def ErrCheckResumeThread(result, func, args): + if result == -1: + raise WinError() + + return args + +ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type + HANDLE # hThread + ) +ResumeThreadFlags = ((1, "hThread"),) +ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32), + ResumeThreadFlags) +ResumeThread.errcheck = ErrCheckResumeThread + +# TerminateJobObject() + +TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + UINT # uExitCode + ) +TerminateJobObjectFlags = ((1, "hJob"), + (1, "uExitCode", 127)) +TerminateJobObject = TerminateJobObjectProto( + ("TerminateJobObject", windll.kernel32), + TerminateJobObjectFlags) +TerminateJobObject.errcheck = ErrCheckBool + +# WaitForSingleObject() + +WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type + HANDLE, # hHandle + DWORD, # dwMilliseconds + ) +WaitForSingleObjectFlags = ((1, "hHandle"), + (1, "dwMilliseconds", -1)) +WaitForSingleObject = WaitForSingleObjectProto( + ("WaitForSingleObject", windll.kernel32), + WaitForSingleObjectFlags) + +INFINITE = -1 +WAIT_TIMEOUT = 0x0102 +WAIT_OBJECT_0 = 0x0 +WAIT_ABANDONED = 0x0080 + +# GetExitCodeProcess() + +GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hProcess + LPDWORD, # lpExitCode + ) +GetExitCodeProcessFlags = ((1, "hProcess"), + (2, "lpExitCode")) +GetExitCodeProcess = GetExitCodeProcessProto( + ("GetExitCodeProcess", windll.kernel32), + GetExitCodeProcessFlags) +GetExitCodeProcess.errcheck = ErrCheckBool diff --git a/IPython/frontend/asyncfrontendbase.py b/IPython/frontend/asyncfrontendbase.py new file mode 100644 index 0000000..3662351 --- /dev/null +++ b/IPython/frontend/asyncfrontendbase.py @@ -0,0 +1,92 @@ +""" +Base front end class for all async frontends. +""" +__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 +#------------------------------------------------------------------------------- +import uuid + +try: + from zope.interface import Interface, Attribute, implements, classProvides +except ImportError, e: + e.message = """%s +________________________________________________________________________________ +zope.interface is required to run asynchronous frontends.""" % e.message + e.args = (e.message, ) + e.args[1:] + +from frontendbase import FrontEndBase, IFrontEnd, IFrontEndFactory + +from IPython.kernel.engineservice import IEngineCore +from IPython.kernel.core.history import FrontEndHistory + +try: + from twisted.python.failure import Failure +except ImportError, e: + e.message = """%s +________________________________________________________________________________ +twisted is required to run asynchronous frontends.""" % e.message + e.args = (e.message, ) + e.args[1:] + + + + +class AsyncFrontEndBase(FrontEndBase): + """ + Overrides FrontEndBase to wrap execute in a deferred result. + All callbacks are made as callbacks on the deferred result. + """ + + implements(IFrontEnd) + classProvides(IFrontEndFactory) + + def __init__(self, engine=None, history=None): + assert(engine==None or IEngineCore.providedBy(engine)) + self.engine = IEngineCore(engine) + if history is None: + self.history = FrontEndHistory(input_cache=['']) + else: + self.history = history + + + def execute(self, block, blockID=None): + """Execute the block and return the deferred result. + + Parameters: + block : {str, AST} + blockID : any + Caller may provide an ID to identify this block. + result['blockID'] := blockID + + Result: + Deferred result of self.interpreter.execute + """ + + if(not self.is_complete(block)): + return Failure(Exception("Block is not compilable")) + + if(blockID == None): + blockID = uuid.uuid4() #random UUID + + d = self.engine.execute(block) + d.addCallback(self._add_history, block=block) + d.addCallbacks(self._add_block_id_for_result, + errback=self._add_block_id_for_failure, + callbackArgs=(blockID,), + errbackArgs=(blockID,)) + d.addBoth(self.update_cell_prompt, blockID=blockID) + d.addCallbacks(self.render_result, + errback=self.render_error) + + return d + + diff --git a/IPython/frontend/cocoa/cocoa_frontend.py b/IPython/frontend/cocoa/cocoa_frontend.py index 801e85d..63b765c 100644 --- a/IPython/frontend/cocoa/cocoa_frontend.py +++ b/IPython/frontend/cocoa/cocoa_frontend.py @@ -40,7 +40,7 @@ from pprint import saferepr import IPython from IPython.kernel.engineservice import ThreadedEngineService -from IPython.frontend.frontendbase import AsyncFrontEndBase +from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase from twisted.internet.threads import blockingCallFromThread from twisted.python.failure import Failure diff --git a/IPython/frontend/frontendbase.py b/IPython/frontend/frontendbase.py index d1e42bb..7012ba8 100644 --- a/IPython/frontend/frontendbase.py +++ b/IPython/frontend/frontendbase.py @@ -24,20 +24,12 @@ import string import uuid import _ast -try: - from zope.interface import Interface, Attribute, implements, classProvides -except ImportError: - #zope.interface is not available - Interface = object - def Attribute(name, doc): pass - def implements(interface): pass - def classProvides(interface): pass +from zopeinterface import Interface, Attribute, implements, classProvides from IPython.kernel.core.history import FrontEndHistory from IPython.kernel.core.util import Bunch from IPython.kernel.engineservice import IEngineCore - ############################################################################## # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or # not @@ -48,6 +40,8 @@ rc.prompt_in2 = r'...' rc.prompt_out = r'Out [$number]: ' ############################################################################## +# Interface definitions +############################################################################## class IFrontEndFactory(Interface): """Factory interface for frontends.""" @@ -61,7 +55,6 @@ class IFrontEndFactory(Interface): pass - class IFrontEnd(Interface): """Interface for frontends. All methods return t.i.d.Deferred""" @@ -74,9 +67,10 @@ class IFrontEnd(Interface): def update_cell_prompt(result, blockID=None): """Subclass may override to update the input prompt for a block. - Since this method will be called as a - twisted.internet.defer.Deferred's callback/errback, - implementations should return result when finished. + + In asynchronous frontends, this method will be called as a + twisted.internet.defer.Deferred's callback/errback. + Implementations should thus return result when finished. Result is a result dict in case of success, and a twisted.python.util.failure.Failure in case of an error @@ -84,7 +78,6 @@ class IFrontEnd(Interface): pass - def render_result(result): """Render the result of an execute call. Implementors may choose the method of rendering. @@ -102,16 +95,17 @@ class IFrontEnd(Interface): pass def render_error(failure): - """Subclasses must override to render the failure. Since this method - will be called as a twisted.internet.defer.Deferred's callback, - implementations should return result when finished. + """Subclasses must override to render the failure. + + In asynchronous frontend, since this method will be called as a + twisted.internet.defer.Deferred's callback. Implementations + should thus return result when finished. blockID = failure.blockID """ pass - def input_prompt(number=''): """Returns the input prompt by subsituting into self.input_prompt_template @@ -142,8 +136,7 @@ class IFrontEnd(Interface): pass - - def get_history_previous(currentBlock): + def get_history_previous(current_block): """Returns the block previous in the history. Saves currentBlock if the history_cursor is currently at the end of the input history""" pass @@ -153,6 +146,20 @@ class IFrontEnd(Interface): pass + def complete(self, line): + """Returns the list of possible completions, and the completed + line. + + The input argument is the full line to be completed. This method + returns both the line completed as much as possible, and the list + of further possible completions (full words). + """ + pass + + +############################################################################## +# Base class for all the frontends. +############################################################################## class FrontEndBase(object): """ @@ -167,9 +174,6 @@ class FrontEndBase(object): history_cursor = 0 - current_indent_level = 0 - - input_prompt_template = string.Template(rc.prompt_in1) output_prompt_template = string.Template(rc.prompt_out) continuation_prompt_template = string.Template(rc.prompt_in2) @@ -295,14 +299,14 @@ class FrontEndBase(object): return result - def get_history_previous(self, currentBlock): + def get_history_previous(self, current_block): """ Returns previous history string and decrement history cursor. """ command = self.history.get_history_item(self.history_cursor - 1) if command is not None: - if(self.history_cursor == len(self.history.input_cache)): - self.history.input_cache[self.history_cursor] = currentBlock + if(self.history_cursor+1 == len(self.history.input_cache)): + self.history.input_cache[self.history_cursor] = current_block self.history_cursor -= 1 return command @@ -322,79 +326,34 @@ class FrontEndBase(object): def update_cell_prompt(self, result, blockID=None): """Subclass may override to update the input prompt for a block. + + This method only really makes sens in asyncrhonous frontend. Since this method will be called as a twisted.internet.defer.Deferred's callback, implementations should return result when finished. """ - return result + raise NotImplementedError def render_result(self, result): - """Subclasses must override to render result. Since this method will - be called as a twisted.internet.defer.Deferred's callback, - implementations should return result when finished. - """ + """Subclasses must override to render result. - return result - - - def render_error(self, failure): - """Subclasses must override to render the failure. Since this method - will be called as a twisted.internet.defer.Deferred's callback, - implementations should return result when finished. + In asynchronous frontends, this method will be called as a + twisted.internet.defer.Deferred's callback. Implementations + should thus return result when finished. """ - return failure - - - -class AsyncFrontEndBase(FrontEndBase): - """ - Overrides FrontEndBase to wrap execute in a deferred result. - All callbacks are made as callbacks on the deferred result. - """ - - implements(IFrontEnd) - classProvides(IFrontEndFactory) - - def __init__(self, engine=None, history=None): - assert(engine==None or IEngineCore.providedBy(engine)) - self.engine = IEngineCore(engine) - if history is None: - self.history = FrontEndHistory(input_cache=['']) - else: - self.history = history + raise NotImplementedError - def execute(self, block, blockID=None): - """Execute the block and return the deferred result. - - Parameters: - block : {str, AST} - blockID : any - Caller may provide an ID to identify this block. - result['blockID'] := blockID + def render_error(self, failure): + """Subclasses must override to render the failure. - Result: - Deferred result of self.interpreter.execute + In asynchronous frontends, this method will be called as a + twisted.internet.defer.Deferred's callback. Implementations + should thus return result when finished. """ - if(not self.is_complete(block)): - from twisted.python.failure import Failure - return Failure(Exception("Block is not compilable")) - - if(blockID == None): - blockID = uuid.uuid4() #random UUID - - d = self.engine.execute(block) - d.addCallback(self._add_history, block=block) - d.addCallback(self._add_block_id_for_result, blockID) - d.addErrback(self._add_block_id_for_failure, blockID) - d.addBoth(self.update_cell_prompt, blockID=blockID) - d.addCallbacks(self.render_result, - errback=self.render_error) - - return d - + raise NotImplementedError diff --git a/IPython/frontend/linefrontendbase.py b/IPython/frontend/linefrontendbase.py new file mode 100644 index 0000000..6f60d75 --- /dev/null +++ b/IPython/frontend/linefrontendbase.py @@ -0,0 +1,294 @@ +""" +Base front end class for all line-oriented frontends, rather than +block-oriented. + +Currently this focuses on synchronous frontends. +""" +__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 +#------------------------------------------------------------------------------- +import re + +import IPython +import sys + +from frontendbase import FrontEndBase +from IPython.kernel.core.interpreter import Interpreter + +def common_prefix(strings): + """ Given a list of strings, return the common prefix between all + these strings. + """ + ref = strings[0] + prefix = '' + for size in range(len(ref)): + test_prefix = ref[:size+1] + for string in strings[1:]: + if not string.startswith(test_prefix): + return prefix + prefix = test_prefix + + return prefix + +#------------------------------------------------------------------------------- +# Base class for the line-oriented front ends +#------------------------------------------------------------------------------- +class LineFrontEndBase(FrontEndBase): + """ Concrete implementation of the FrontEndBase class. This is meant + to be the base class behind all the frontend that are line-oriented, + rather than block-oriented. + """ + + # We need to keep the prompt number, to be able to increment + # it when there is an exception. + prompt_number = 1 + + # We keep a reference to the last result: it helps testing and + # programatic control of the frontend. + last_result = dict(number=0) + + # The input buffer being edited + input_buffer = '' + + # Set to true for debug output + debug = False + + # A banner to print at startup + banner = None + + #-------------------------------------------------------------------------- + # FrontEndBase interface + #-------------------------------------------------------------------------- + + def __init__(self, shell=None, history=None, banner=None, *args, **kwargs): + if shell is None: + shell = Interpreter() + FrontEndBase.__init__(self, shell=shell, history=history) + + if banner is not None: + self.banner = banner + if self.banner is not None: + self.write(self.banner, refresh=False) + + self.new_prompt(self.input_prompt_template.substitute(number=1)) + + + def complete(self, line): + """Complete line in engine's user_ns + + Parameters + ---------- + line : string + + Result + ------ + The replacement for the line and the list of possible completions. + """ + completions = self.shell.complete(line) + complete_sep = re.compile('[\s\{\}\[\]\(\)\=]') + if completions: + prefix = common_prefix(completions) + residual = complete_sep.split(line)[:-1] + line = line[:-len(residual)] + prefix + return line, completions + + + def render_result(self, result): + """ Frontend-specific rendering of the result of a calculation + that has been sent to an engine. + """ + if 'stdout' in result and result['stdout']: + self.write('\n' + result['stdout']) + if 'display' in result and result['display']: + self.write("%s%s\n" % ( + self.output_prompt_template.substitute( + number=result['number']), + result['display']['pprint'] + ) ) + + + def render_error(self, failure): + """ Frontend-specific rendering of error. + """ + self.write('\n\n'+str(failure)+'\n\n') + return failure + + + def is_complete(self, string): + """ Check if a string forms a complete, executable set of + commands. + + For the line-oriented frontend, multi-line code is not executed + as soon as it is complete: the users has to enter two line + returns. + """ + if string in ('', '\n'): + # Prefiltering, eg through ipython0, may return an empty + # string although some operations have been accomplished. We + # thus want to consider an empty string as a complete + # statement. + return True + elif ( len(self.input_buffer.split('\n'))>2 + 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') + + + def write(self, string, refresh=True): + """ Write some characters to the display. + + Subclass should overide this method. + + The refresh keyword argument is used in frontends with an + event loop, to choose whether the write should trigget an UI + refresh, and thus be syncrhonous, or not. + """ + print >>sys.__stderr__, string + + + def execute(self, python_string, raw_string=None): + """ Stores the raw_string in the history, and sends the + python string to the interpreter. + """ + if raw_string is None: + raw_string = python_string + # Create a false result, in case there is an exception + self.last_result = dict(number=self.prompt_number) + try: + self.history.input_cache[-1] = raw_string.rstrip() + result = self.shell.execute(python_string) + self.last_result = result + self.render_result(result) + except: + self.show_traceback() + finally: + self.after_execute() + + #-------------------------------------------------------------------------- + # LineFrontEndBase interface + #-------------------------------------------------------------------------- + + def prefilter_input(self, string): + """ Priflter the input to turn it in valid python. + """ + string = string.replace('\r\n', '\n') + string = string.replace('\t', 4*' ') + # Clean the trailing whitespace + string = '\n'.join(l.rstrip() for l in string.split('\n')) + return string + + + def after_execute(self): + """ All the operations required after an execution to put the + terminal back in a shape where it is usable. + """ + self.prompt_number += 1 + self.new_prompt(self.input_prompt_template.substitute( + number=(self.last_result['number'] + 1))) + # Start a new empty history entry + self._add_history(None, '') + self.history_cursor = len(self.history.input_cache) - 1 + + + def complete_current_input(self): + """ Do code completion on current line. + """ + if self.debug: + print >>sys.__stdout__, "complete_current_input", + line = self.input_buffer + new_line, completions = self.complete(line) + if len(completions)>1: + self.write_completion(completions) + self.input_buffer = new_line + if self.debug: + print >>sys.__stdout__, completions + + + def get_line_width(self): + """ Return the width of the line in characters. + """ + return 80 + + + def write_completion(self, possibilities): + """ Write the list of possible completions. + """ + current_buffer = self.input_buffer + + self.write('\n') + max_len = len(max(possibilities, key=len)) + 1 + + # Now we check how much symbol we can put on a line... + chars_per_line = self.get_line_width() + symbols_per_line = max(1, chars_per_line/max_len) + + pos = 1 + buf = [] + for symbol in possibilities: + if pos < symbols_per_line: + buf.append(symbol.ljust(max_len)) + pos += 1 + else: + buf.append(symbol.rstrip() + '\n') + pos = 1 + self.write(''.join(buf)) + self.new_prompt(self.input_prompt_template.substitute( + number=self.last_result['number'] + 1)) + self.input_buffer = current_buffer + + + def new_prompt(self, prompt): + """ Prints a prompt and starts a new editing buffer. + + Subclasses should use this method to make sure that the + terminal is put in a state favorable for a new line + input. + """ + self.input_buffer = '' + self.write(prompt) + + + #-------------------------------------------------------------------------- + # Private API + #-------------------------------------------------------------------------- + + def _on_enter(self): + """ Called when the return key is pressed in a line editing + buffer. + """ + current_buffer = self.input_buffer + cleaned_buffer = self.prefilter_input(current_buffer) + if self.is_complete(cleaned_buffer): + self.execute(cleaned_buffer, raw_string=current_buffer) + else: + self.input_buffer += self._get_indent_string( + current_buffer[:-1]) + if current_buffer[:-1].split('\n')[-1].rstrip().endswith(':'): + self.input_buffer += '\t' + + + def _get_indent_string(self, string): + """ Return the string of whitespace that prefixes a line. Used to + add the right amount of indendation when creating a new line. + """ + string = string.replace('\t', ' '*4) + string = string.split('\n')[-1] + indent_chars = len(string) - len(string.lstrip()) + indent_string = '\t'*(indent_chars // 4) + \ + ' '*(indent_chars % 4) + + return indent_string + + diff --git a/IPython/frontend/prefilterfrontend.py b/IPython/frontend/prefilterfrontend.py new file mode 100644 index 0000000..f1b941c --- /dev/null +++ b/IPython/frontend/prefilterfrontend.py @@ -0,0 +1,221 @@ +""" +Frontend class that uses IPython0 to prefilter the inputs. + +Using the IPython0 mechanism gives us access to the magics. + +This is a transitory class, used here to do the transition between +ipython0 and ipython1. This class is meant to be short-lived as more +functionnality is abstracted out of ipython0 in reusable functions and +is added on the interpreter. This class can be a used to guide this +refactoring. +""" +__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 +#------------------------------------------------------------------------------- +import sys + +from linefrontendbase import LineFrontEndBase, common_prefix + +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.genutils import Term +import pydoc +import os + + +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): + system_call_function("%s %s" % (command, args)) + return my_system_call + +#------------------------------------------------------------------------------- +# Frontend class using ipython0 to do the prefiltering. +#------------------------------------------------------------------------------- +class PrefilterFrontEnd(LineFrontEndBase): + """ Class that uses ipython0 to do prefilter the input, do the + completion and the magics. + + The core trick is to use an ipython0 instance to prefilter the + input, and share the namespace between the interpreter instance used + to execute the statements and the ipython0 used for code + completion... + """ + + def __init__(self, ipython0=None, *args, **kwargs): + """ Parameters: + ----------- + + ipython0: an optional ipython0 instance to use for command + prefiltering and completion. + """ + 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=[]) + self.ipython0 = ipython0 + # Set the pager: + self.ipython0.set_hook('show_in_pager', + lambda s, string: self.write("\n" + string)) + self.ipython0.write = self.write + self._ip = _ip = IPApi(self.ipython0) + # Make sure the raw system call doesn't get called, as we don't + # have a stdin accessible. + self._ip.system = self.system_call + # XXX: Muck around with magics so that they work better + # in our environment + self.ipython0.magic_ls = mk_system_call(self.system_call, + '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 + """ +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 = self.ipython0.user_ns + self.shell.user_global_ns = self.ipython0.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, + ) + + #-------------------------------------------------------------------------- + # FrontEndBase interface + #-------------------------------------------------------------------------- + + def show_traceback(self): + """ Use ipython0 to capture the last traceback and display it. + """ + self.capture_output() + self.ipython0.showtraceback() + self.release_output() + + + def execute(self, python_string, raw_string=None): + if self.debug: + print 'Executing Python code:', repr(python_string) + self.capture_output() + LineFrontEndBase.execute(self, python_string, + raw_string=raw_string) + self.release_output() + + + def save_output_hooks(self): + """ Store all the output hooks we can think of, to be able to + restore them. + + We need to do this early, as starting the ipython0 instance will + screw ouput hooks. + """ + self.__old_cout_write = Term.cout.write + self.__old_cerr_write = Term.cerr.write + self.__old_stdout = sys.stdout + self.__old_stderr= sys.stderr + self.__old_help_output = pydoc.help.output + self.__old_display_hook = sys.displayhook + + + def capture_output(self): + """ Capture all the output mechanisms we can think of. + """ + self.save_output_hooks() + Term.cout.write = self.write + Term.cerr.write = self.write + sys.stdout = Term.cout + sys.stderr = Term.cerr + pydoc.help.output = self.shell.output_trap.out + + + def release_output(self): + """ Release all the different captures we have made. + """ + Term.cout.write = self.__old_cout_write + Term.cerr.write = self.__old_cerr_write + sys.stdout = self.__old_stdout + sys.stderr = self.__old_stderr + pydoc.help.output = self.__old_help_output + sys.displayhook = self.__old_display_hook + + + def complete(self, line): + word = line.split('\n')[-1].split(' ')[-1] + completions = self.ipython0.complete(word) + # FIXME: The proper sort should be done in the complete method. + key = lambda x: x.replace('_', '') + completions.sort(key=key) + if completions: + prefix = common_prefix(completions) + line = line[:-len(word)] + prefix + return line, completions + + + #-------------------------------------------------------------------------- + # LineFrontEndBase interface + #-------------------------------------------------------------------------- + + def prefilter_input(self, input_string): + """ Using IPython0 to prefilter the commands to turn them + in executable statements that are valid Python strings. + """ + input_string = LineFrontEndBase.prefilter_input(self, input_string) + filtered_lines = [] + # The IPython0 prefilters sometime produce output. We need to + # capture it. + self.capture_output() + self.last_result = dict(number=self.prompt_number) + try: + for line in input_string.split('\n'): + filtered_lines.append( + self.ipython0.prefilter(line, False).rstrip()) + except: + # XXX: probably not the right thing to do. + self.ipython0.showsyntaxerror() + self.after_execute() + finally: + self.release_output() + + # Clean up the trailing whitespace, to avoid indentation errors + filtered_string = '\n'.join(filtered_lines) + return filtered_string + + + #-------------------------------------------------------------------------- + # PrefilterFrontEnd interface + #-------------------------------------------------------------------------- + + def system_call(self, command_string): + """ Allows for frontend to define their own system call, to be + able capture output and redirect input. + """ + return os.system(command_string) + + + def do_exit(self): + """ Exit the shell, cleanup and save the history. + """ + self.ipython0.atexit_operations() + diff --git a/IPython/frontend/tests/test_frontendbase.py b/IPython/frontend/tests/test_frontendbase.py index 54c470d..42d3856 100644 --- a/IPython/frontend/tests/test_frontendbase.py +++ b/IPython/frontend/tests/test_frontendbase.py @@ -16,10 +16,11 @@ __docformat__ = "restructuredtext en" #--------------------------------------------------------------------------- import unittest -from IPython.frontend import frontendbase +from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase +from IPython.frontend import frontendbase from IPython.kernel.engineservice import EngineService -class FrontEndCallbackChecker(frontendbase.AsyncFrontEndBase): +class FrontEndCallbackChecker(AsyncFrontEndBase): """FrontEndBase subclass for checking callbacks""" def __init__(self, engine=None, history=None): super(FrontEndCallbackChecker, self).__init__(engine=engine, @@ -53,7 +54,7 @@ class TestAsyncFrontendBase(unittest.TestCase): def test_implements_IFrontEnd(self): assert(frontendbase.IFrontEnd.implementedBy( - frontendbase.AsyncFrontEndBase)) + AsyncFrontEndBase)) def test_is_complete_returns_False_for_incomplete_block(self): diff --git a/IPython/frontend/tests/test_prefilterfrontend.py b/IPython/frontend/tests/test_prefilterfrontend.py new file mode 100644 index 0000000..f786170 --- /dev/null +++ b/IPython/frontend/tests/test_prefilterfrontend.py @@ -0,0 +1,157 @@ +# encoding: utf-8 +""" +Test process execution and IO redirection. +""" + +__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. +#------------------------------------------------------------------------------- + +from cStringIO import StringIO +import string + +from IPython.ipapi import get as get_ipython0 +from IPython.frontend.prefilterfrontend import PrefilterFrontEnd + +class TestPrefilterFrontEnd(PrefilterFrontEnd): + + input_prompt_template = string.Template('') + output_prompt_template = string.Template('') + banner = '' + + def __init__(self): + ipython0 = get_ipython0().IP + self.out = StringIO() + PrefilterFrontEnd.__init__(self, ipython0=ipython0) + # Clean up the namespace for isolation between tests + user_ns = self.ipython0.user_ns + # We need to keep references to things so that they don't + # get garbage collected (this stinks). + self.shadow_ns = dict() + for i in self.ipython0.magic_who_ls(): + self.shadow_ns[i] = user_ns.pop(i) + # Some more code for isolation (yeah, crazy) + self._on_enter() + self.out.flush() + self.out.reset() + self.out.truncate() + + def write(self, string, *args, **kwargs): + self.out.write(string) + + def _on_enter(self): + self.input_buffer += '\n' + PrefilterFrontEnd._on_enter(self) + + +def test_execution(): + """ Test execution of a command. + """ + f = TestPrefilterFrontEnd() + f.input_buffer = 'print 1' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '1\n' + + +def test_multiline(): + """ Test execution of a multiline command. + """ + f = TestPrefilterFrontEnd() + f.input_buffer = 'if True:' + f._on_enter() + f.input_buffer += 'print 1' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '1\n' + f = TestPrefilterFrontEnd() + f.input_buffer='(1 +' + f._on_enter() + f.input_buffer += '0)' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '1\n' + + +def test_capture(): + """ Test the capture of output in different channels. + """ + # Test on the OS-level stdout, stderr. + f = TestPrefilterFrontEnd() + f.input_buffer = \ + 'import os; out=os.fdopen(1, "w"); out.write("1") ; out.flush()' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '1' + f = TestPrefilterFrontEnd() + f.input_buffer = \ + 'import os; out=os.fdopen(2, "w"); out.write("1") ; out.flush()' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == '1' + + +def test_magic(): + """ Test the magic expansion and history. + + This test is fairly fragile and will break when magics change. + """ + f = TestPrefilterFrontEnd() + f.input_buffer += '%who' + f._on_enter() + out_value = f.out.getvalue() + assert out_value == 'Interactive namespace is empty.\n' + + +def test_help(): + """ Test object inspection. + """ + f = TestPrefilterFrontEnd() + f.input_buffer += "def f():" + f._on_enter() + f.input_buffer += "'foobar'" + f._on_enter() + f.input_buffer += "pass" + f._on_enter() + f._on_enter() + f.input_buffer += "f?" + f._on_enter() + assert 'traceback' not in f.last_result + ## XXX: ipython doctest magic breaks this. I have no clue why + #out_value = f.out.getvalue() + #assert out_value.split()[-1] == 'foobar' + + +def test_completion(): + """ Test command-line completion. + """ + f = TestPrefilterFrontEnd() + f.input_buffer = 'zzza = 1' + f._on_enter() + f.input_buffer = 'zzzb = 2' + f._on_enter() + f.input_buffer = 'zz' + f.complete_current_input() + out_value = f.out.getvalue() + assert out_value == '\nzzza zzzb ' + assert f.input_buffer == 'zzz' + + +if __name__ == '__main__': + test_magic() + test_help() + test_execution() + test_multiline() + test_capture() + test_completion() diff --git a/IPython/frontend/tests/test_process.py b/IPython/frontend/tests/test_process.py new file mode 100644 index 0000000..b275dff --- /dev/null +++ b/IPython/frontend/tests/test_process.py @@ -0,0 +1,63 @@ +# encoding: utf-8 +""" +Test process execution and IO redirection. +""" + +__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. +#------------------------------------------------------------------------------- + +from cStringIO import StringIO +from time import sleep +import sys + +from IPython.frontend._process import PipedProcess + +def test_capture_out(): + """ A simple test to see if we can execute a process and get the output. + """ + s = StringIO() + p = PipedProcess('echo 1', out_callback=s.write, ) + p.start() + p.join() + assert s.getvalue() == '1\n' + + +def test_io(): + """ Checks that we can send characters on stdin to the process. + """ + s = StringIO() + p = PipedProcess(sys.executable + ' -c "a = raw_input(); print a"', + out_callback=s.write, ) + p.start() + test_string = '12345\n' + while not hasattr(p, 'process'): + sleep(0.1) + p.process.stdin.write(test_string) + p.join() + assert s.getvalue() == test_string + + +def test_kill(): + """ Check that we can kill a process, and its subprocess. + """ + s = StringIO() + p = PipedProcess(sys.executable + ' -c "a = raw_input();"', + out_callback=s.write, ) + p.start() + while not hasattr(p, 'process'): + sleep(0.1) + p.process.kill() + assert p.process.poll() is not None + + +if __name__ == '__main__': + test_capture_out() + test_io() + test_kill() + diff --git a/IPython/frontend/wx/__init__.py b/IPython/frontend/wx/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/frontend/wx/__init__.py diff --git a/IPython/frontend/wx/console_widget.py b/IPython/frontend/wx/console_widget.py new file mode 100644 index 0000000..3afda8a --- /dev/null +++ b/IPython/frontend/wx/console_widget.py @@ -0,0 +1,428 @@ +# encoding: utf-8 +""" +A Wx widget to act as a console and input commands. + +This widget deals with prompts and provides an edit buffer +restricted to after the last prompt. +""" + +__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 +#------------------------------------------------------------------------------- + +import wx +import wx.stc as stc + +from wx.py import editwindow +import sys +LINESEP = '\n' +if sys.platform == 'win32': + LINESEP = '\n\r' + +import re + +# FIXME: Need to provide an API for non user-generated display on the +# screen: this should not be editable by the user. + +_DEFAULT_SIZE = 10 +if sys.platform == 'darwin': + _DEFAULT_STYLE = 12 + +_DEFAULT_STYLE = { + 'stdout' : 'fore:#0000FF', + 'stderr' : 'fore:#007f00', + 'trace' : 'fore:#FF0000', + + 'default' : 'size:%d' % _DEFAULT_SIZE, + 'bracegood' : 'fore:#00AA00,back:#000000,bold', + 'bracebad' : 'fore:#FF0000,back:#000000,bold', + + # properties for the various Python lexer styles + 'comment' : 'fore:#007F00', + 'number' : 'fore:#007F7F', + 'string' : 'fore:#7F007F,italic', + 'char' : 'fore:#7F007F,italic', + 'keyword' : 'fore:#00007F,bold', + 'triple' : 'fore:#7F0000', + 'tripledouble' : 'fore:#7F0000', + 'class' : 'fore:#0000FF,bold,underline', + 'def' : 'fore:#007F7F,bold', + 'operator' : 'bold' + } + +# new style numbers +_STDOUT_STYLE = 15 +_STDERR_STYLE = 16 +_TRACE_STYLE = 17 + + +# system colors +#SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND) + +#------------------------------------------------------------------------------- +# The console widget class +#------------------------------------------------------------------------------- +class ConsoleWidget(editwindow.EditWindow): + """ Specialized styled text control view for console-like workflow. + + This widget is mainly interested in dealing with the prompt and + keeping the cursor inside the editing line. + """ + + # This is where the title captured from the ANSI escape sequences are + # stored. + title = 'Console' + + # The buffer being edited. + def _set_input_buffer(self, string): + self.SetSelection(self.current_prompt_pos, self.GetLength()) + self.ReplaceSelection(string) + self.GotoPos(self.GetLength()) + + def _get_input_buffer(self): + """ Returns the text in current edit buffer. + """ + input_buffer = self.GetTextRange(self.current_prompt_pos, + self.GetLength()) + input_buffer = input_buffer.replace(LINESEP, '\n') + return input_buffer + + input_buffer = property(_get_input_buffer, _set_input_buffer) + + style = _DEFAULT_STYLE.copy() + + # Translation table from ANSI escape sequences to color. Override + # this to specify your colors. + ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'], + '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'], + '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'], + '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'], + '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'], + '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'], + '1;34': [12, 'LIGHT BLUE'], '1;35': + [13, 'MEDIUM VIOLET RED'], + '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']} + + # The color of the carret (call _apply_style() after setting) + carret_color = 'BLACK' + + #-------------------------------------------------------------------------- + # Public API + #-------------------------------------------------------------------------- + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=0, ): + editwindow.EditWindow.__init__(self, parent, id, pos, size, style) + self._configure_scintilla() + + self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + self.Bind(wx.EVT_KEY_UP, self._on_key_up) + + + def write(self, text, refresh=True): + """ Write given text to buffer, while translating the ansi escape + sequences. + """ + # 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 + title = self.title_pat.split(text) + if len(title)>1: + self.title = title[-2] + + text = self.title_pat.sub('', text) + segments = self.color_pat.split(text) + segment = segments.pop(0) + self.GotoPos(self.GetLength()) + self.StartStyling(self.GetLength(), 0xFF) + try: + self.AppendText(segment) + except UnicodeDecodeError: + # XXX: Do I really want to skip the exception? + pass + + if segments: + for ansi_tag, text in zip(segments[::2], segments[1::2]): + self.StartStyling(self.GetLength(), 0xFF) + try: + self.AppendText(text) + except UnicodeDecodeError: + # XXX: Do I really want to skip the exception? + pass + + if ansi_tag not in self.ANSI_STYLES: + style = 0 + else: + style = self.ANSI_STYLES[ansi_tag][0] + + self.SetStyling(len(text), style) + + self.GotoPos(self.GetLength()) + if refresh: + # Maybe this is faster than wx.Yield() + self.ProcessEvent(wx.PaintEvent()) + #wx.Yield() + + + def new_prompt(self, prompt): + """ Prints a prompt at start of line, and move the start of the + current block there. + + The prompt can be given with ascii escape sequences. + """ + self.write(prompt, refresh=False) + # 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() + + + def scroll_to_bottom(self): + maxrange = self.GetScrollRange(wx.VERTICAL) + self.ScrollLines(maxrange) + + + def pop_completion(self, possibilities, offset=0): + """ Pops up an autocompletion menu. Offset is the offset + in characters of the position at which the menu should + appear, relativ to the cursor. + """ + self.AutoCompSetIgnoreCase(False) + self.AutoCompSetAutoHide(False) + self.AutoCompSetMaxHeight(len(possibilities)) + self.AutoCompShow(offset, " ".join(possibilities)) + + + def get_line_width(self): + """ Return the width of the line in characters. + """ + return self.GetSize()[0]/self.GetCharWidth() + + #-------------------------------------------------------------------------- + # EditWindow API + #-------------------------------------------------------------------------- + + def OnUpdateUI(self, event): + """ Override the OnUpdateUI of the EditWindow class, to prevent + syntax highlighting both for faster redraw, and for more + consistent look and feel. + """ + + #-------------------------------------------------------------------------- + # Private API + #-------------------------------------------------------------------------- + + def _apply_style(self): + """ Applies the colors for the different text elements and the + carret. + """ + self.SetCaretForeground(self.carret_color) + + #self.StyleClearAll() + self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, + "fore:#FF0000,back:#0000FF,bold") + self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, + "fore:#000000,back:#FF0000,bold") + + for style in self.ANSI_STYLES.values(): + self.StyleSetSpec(style[0], "bold,fore:%s" % style[1]) + + + def _configure_scintilla(self): + self.SetEOLMode(stc.STC_EOL_LF) + + # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside + # the widget + self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN) + self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT) + # Also allow Ctrl Shift "=" for poor non US keyboard users. + self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT, + stc.STC_CMD_ZOOMIN) + + # Keys: we need to clear some of the keys the that don't play + # well with a console. + self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL) + self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL) + self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL) + self.CmdKeyClear(ord('A'), stc.STC_SCMOD_CTRL) + + self.SetEOLMode(stc.STC_EOL_CRLF) + self.SetWrapMode(stc.STC_WRAP_CHAR) + self.SetWrapMode(stc.STC_WRAP_WORD) + self.SetBufferedDraw(True) + self.SetUseAntiAliasing(True) + self.SetLayoutCache(stc.STC_CACHE_PAGE) + self.SetUndoCollection(False) + self.SetUseTabs(True) + self.SetIndent(4) + self.SetTabWidth(4) + + # we don't want scintilla's autocompletion to choose + # automaticaly out of a single choice list, as we pop it up + # automaticaly + self.AutoCompSetChooseSingle(False) + self.AutoCompSetMaxHeight(10) + # XXX: this doesn't seem to have an effect. + self.AutoCompSetFillUps('\n') + + self.SetMargins(3, 3) #text is moved away from border with 3px + # Suppressing Scintilla margins + self.SetMarginWidth(0, 0) + self.SetMarginWidth(1, 0) + self.SetMarginWidth(2, 0) + + self._apply_style() + + # Xterm escape sequences + self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?') + self.title_pat = re.compile('\x1b]0;(.*?)\x07') + + #self.SetEdgeMode(stc.STC_EDGE_LINE) + #self.SetEdgeColumn(80) + + # styles + p = self.style + self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default']) + self.StyleClearAll() + self.StyleSetSpec(_STDOUT_STYLE, p['stdout']) + self.StyleSetSpec(_STDERR_STYLE, p['stderr']) + self.StyleSetSpec(_TRACE_STYLE, p['trace']) + + self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood']) + self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad']) + self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment']) + self.StyleSetSpec(stc.STC_P_NUMBER, p['number']) + self.StyleSetSpec(stc.STC_P_STRING, p['string']) + self.StyleSetSpec(stc.STC_P_CHARACTER, p['char']) + self.StyleSetSpec(stc.STC_P_WORD, p['keyword']) + self.StyleSetSpec(stc.STC_P_WORD2, p['keyword']) + self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple']) + self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble']) + self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class']) + self.StyleSetSpec(stc.STC_P_DEFNAME, p['def']) + self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator']) + self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment']) + + def _on_key_down(self, event, skip=True): + """ Key press callback used for correcting behavior for + console-like interfaces: the cursor is constraint to be after + the last prompt. + + Return True if event as been catched. + """ + catched = True + # Intercept some specific keys. + if event.KeyCode == ord('L') and event.ControlDown() : + self.scroll_to_bottom() + elif event.KeyCode == ord('K') and event.ControlDown() : + self.input_buffer = '' + elif event.KeyCode == ord('A') and event.ControlDown() : + self.GotoPos(self.GetLength()) + self.SetSelectionStart(self.current_prompt_pos) + self.SetSelectionEnd(self.GetCurrentPos()) + catched = True + elif event.KeyCode == ord('E') and event.ControlDown() : + self.GotoPos(self.GetLength()) + catched = True + elif event.KeyCode == wx.WXK_PAGEUP: + self.ScrollPages(-1) + elif event.KeyCode == wx.WXK_PAGEDOWN: + self.ScrollPages(1) + elif event.KeyCode == wx.WXK_UP and event.ShiftDown(): + self.ScrollLines(-1) + elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown(): + self.ScrollLines(1) + else: + catched = False + + if self.AutoCompActive(): + event.Skip() + else: + if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \ + event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): + catched = True + self.CallTipCancel() + self.write('\n', refresh=False) + # Under windows scintilla seems to be doing funny stuff to the + # line returns here, but the getter for input_buffer filters + # this out. + if sys.platform == 'win32': + self.input_buffer = self.input_buffer + self._on_enter() + + elif event.KeyCode == wx.WXK_HOME: + if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN): + self.GotoPos(self.current_prompt_pos) + catched = True + + elif event.Modifiers == wx.MOD_SHIFT: + # FIXME: This behavior is not ideal: if the selection + # is already started, it will jump. + self.SetSelectionStart(self.current_prompt_pos) + self.SetSelectionEnd(self.GetCurrentPos()) + catched = True + + elif event.KeyCode == wx.WXK_UP: + if self.GetCurrentLine() > self.current_prompt_line: + if self.GetCurrentLine() == self.current_prompt_line + 1 \ + and self.GetColumn(self.GetCurrentPos()) < \ + self.GetColumn(self.current_prompt_pos): + self.GotoPos(self.current_prompt_pos) + else: + event.Skip() + catched = True + + elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK): + if self.GetCurrentPos() > self.current_prompt_pos: + event.Skip() + catched = True + + if skip and not catched: + # Put the cursor back in the edit region + if self.GetCurrentPos() < self.current_prompt_pos: + self.GotoPos(self.current_prompt_pos) + else: + event.Skip() + + return catched + + + def _on_key_up(self, event, skip=True): + """ If cursor is outside the editing region, put it back. + """ + event.Skip() + if self.GetCurrentPos() < self.current_prompt_pos: + self.GotoPos(self.current_prompt_pos) + + + +if __name__ == '__main__': + # Some simple code to test the console widget. + class MainWindow(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, size=(300,250)) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.console_widget = ConsoleWidget(self) + self._sizer.Add(self.console_widget, 1, wx.EXPAND) + self.SetSizer(self._sizer) + self.SetAutoLayout(1) + self.Show(True) + + app = wx.PySimpleApp() + w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget') + w.SetSize((780, 460)) + w.Show() + + app.MainLoop() + + diff --git a/IPython/frontend/wx/ipythonx.py b/IPython/frontend/wx/ipythonx.py new file mode 100644 index 0000000..df81443 --- /dev/null +++ b/IPython/frontend/wx/ipythonx.py @@ -0,0 +1,110 @@ +""" +Entry point for a simple application giving a graphical frontend to +ipython. +""" + +try: + import wx +except ImportError, e: + e.message = """%s +________________________________________________________________________________ +You need wxPython to run this application. +""" % e.message + e.args = (e.message, ) + e.args[1:] + raise e + +from wx_frontend import WxController +import __builtin__ + + +class IPythonXController(WxController): + """ Sub class of WxController that adds some application-specific + bindings. + """ + + debug = False + + def __init__(self, *args, **kwargs): + WxController.__init__(self, *args, **kwargs) + self.ipython0.ask_exit = self.do_exit + # Scroll to top + maxrange = self.GetScrollRange(wx.VERTICAL) + self.ScrollLines(-maxrange) + + + def _on_key_down(self, event, skip=True): + # Intercept Ctrl-D to quit + if event.KeyCode == ord('D') and event.ControlDown() and \ + self.input_buffer == '' and \ + self._input_state == 'readline': + wx.CallAfter(self.ask_exit) + else: + WxController._on_key_down(self, event, skip=skip) + + + def ask_exit(self): + """ Ask the user whether to exit. + """ + self._input_state = 'subprocess' + self.write('\n', refresh=False) + self.capture_output() + self.ipython0.shell.exit() + self.release_output() + if not self.ipython0.exit_now: + wx.CallAfter(self.new_prompt, + self.input_prompt_template.substitute( + number=self.last_result['number'] + 1)) + else: + wx.CallAfter(wx.GetApp().Exit) + self.write('Exiting ...', refresh=False) + + + def do_exit(self): + """ Exits the interpreter, kills the windows. + """ + WxController.do_exit(self) + self.release_output() + wx.CallAfter(wx.Exit) + + + +class IPythonX(wx.Frame): + """ Main frame of the IPythonX app. + """ + + def __init__(self, parent, id, title, debug=False): + wx.Frame.__init__(self, parent, id, title, size=(300,250)) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.shell = IPythonXController(self, debug=debug) + self._sizer.Add(self.shell, 1, wx.EXPAND) + self.SetSizer(self._sizer) + self.SetAutoLayout(1) + self.Show(True) + + +def main(): + from optparse import OptionParser + usage = """usage: %prog [options] + +Simple graphical frontend to IPython, using WxWidgets.""" + parser = OptionParser(usage=usage) + parser.add_option("-d", "--debug", + action="store_true", dest="debug", default=False, + help="Enable debug message for the wx frontend.") + + options, args = parser.parse_args() + + # Clear the options, to avoid having the ipython0 instance complain + import sys + sys.argv = sys.argv[:1] + + app = wx.PySimpleApp() + frame = IPythonX(None, wx.ID_ANY, 'IPythonX', debug=options.debug) + frame.shell.SetFocus() + frame.shell.app = app + frame.SetSize((680, 460)) + + app.MainLoop() + +if __name__ == '__main__': + main() diff --git a/IPython/frontend/wx/wx_frontend.py b/IPython/frontend/wx/wx_frontend.py new file mode 100644 index 0000000..b695897 --- /dev/null +++ b/IPython/frontend/wx/wx_frontend.py @@ -0,0 +1,510 @@ +# encoding: utf-8 -*- test-case-name: +# FIXME: Need to add tests. +# ipython1.frontend.wx.tests.test_wx_frontend -*- + +"""Classes to provide a Wx frontend to the +IPython.kernel.core.interpreter. + +This class inherits from ConsoleWidget, that provides a console-like +widget to provide a text-rendering widget suitable for a terminal. +""" + +__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 +#------------------------------------------------------------------------------- + +# Major library imports +import re +import __builtin__ +from time import sleep +import sys +from threading import Lock +import string + +import wx +from wx import stc + +# Ipython-specific imports. +from IPython.frontend._process import PipedProcess +from console_widget import ConsoleWidget +from IPython.frontend.prefilterfrontend import PrefilterFrontEnd + +#------------------------------------------------------------------------------- +# Constants +#------------------------------------------------------------------------------- + +_COMPLETE_BUFFER_BG = '#FAFAF1' # Nice green +_INPUT_BUFFER_BG = '#FDFFD3' # Nice yellow +_ERROR_BG = '#FFF1F1' # Nice red + +_COMPLETE_BUFFER_MARKER = 31 +_ERROR_MARKER = 30 +_INPUT_MARKER = 29 + +prompt_in1 = \ + '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02$number\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02' + +prompt_out = \ + '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02$number\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02' + +#------------------------------------------------------------------------------- +# Classes to implement the Wx frontend +#------------------------------------------------------------------------------- +class WxController(ConsoleWidget, PrefilterFrontEnd): + """Classes to provide a Wx frontend to the + IPython.kernel.core.interpreter. + + This class inherits from ConsoleWidget, that provides a console-like + widget to provide a text-rendering widget suitable for a terminal. + """ + + output_prompt_template = string.Template(prompt_out) + + input_prompt_template = string.Template(prompt_in1) + + # Print debug info on what is happening to the console. + debug = False + + # The title of the terminal, as captured through the ANSI escape + # sequences. + def _set_title(self, title): + return self.Parent.SetTitle(title) + + def _get_title(self): + return self.Parent.GetTitle() + + title = property(_get_title, _set_title) + + + # The buffer being edited. + # We are duplicating the definition here because of multiple + # inheritence + def _set_input_buffer(self, string): + ConsoleWidget._set_input_buffer(self, string) + self._colorize_input_buffer() + + def _get_input_buffer(self): + """ Returns the text in current edit buffer. + """ + return ConsoleWidget._get_input_buffer(self) + + input_buffer = property(_get_input_buffer, _set_input_buffer) + + + #-------------------------------------------------------------------------- + # Private Attributes + #-------------------------------------------------------------------------- + + # A flag governing the behavior of the input. Can be: + # + # 'readline' for readline-like behavior with a prompt + # and an edit buffer. + # 'raw_input' similar to readline, but triggered by a raw-input + # call. Can be used by subclasses to act differently. + # 'subprocess' for sending the raw input directly to a + # subprocess. + # 'buffering' for buffering of the input, that will be used + # when the input state switches back to another state. + _input_state = 'readline' + + # 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 + _out_buffer = [] + + # A lock to lock the _out_buffer to make sure we don't empty it + # while it is being swapped + _out_buffer_lock = Lock() + + _markers = dict() + + #-------------------------------------------------------------------------- + # Public API + #-------------------------------------------------------------------------- + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.CLIP_CHILDREN, + *args, **kwds): + """ Create Shell instance. + """ + ConsoleWidget.__init__(self, parent, id, pos, size, style) + PrefilterFrontEnd.__init__(self, **kwds) + + # Marker for complete buffer. + self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND, + background=_COMPLETE_BUFFER_BG) + # Marker for current input buffer. + self.MarkerDefine(_INPUT_MARKER, stc.STC_MARK_BACKGROUND, + background=_INPUT_BUFFER_BG) + # Marker for tracebacks. + 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) + + if 'debug' in kwds: + self.debug = kwds['debug'] + kwds.pop('debug') + + # Inject self in namespace, for debug + if self.debug: + self.shell.user_ns['self'] = self + + + def raw_input(self, prompt): + """ A replacement from python's raw_input. + """ + self.new_prompt(prompt) + self._input_state = 'raw_input' + if hasattr(self, '_cursor'): + del self._cursor + self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS)) + self.waiting = True + self.__old_on_enter = self._on_enter + def my_on_enter(): + self.waiting = False + self._on_enter = my_on_enter + # XXX: Busy waiting, ugly. + while self.waiting: + wx.Yield() + sleep(0.1) + self._on_enter = self.__old_on_enter + self._input_state = 'buffering' + self._cursor = wx.BusyCursor() + return self.input_buffer.rstrip('\n') + + + def system_call(self, command_string): + self._input_state = 'subprocess' + 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 + wx.Yield() + while self._running_process: + wx.Yield() + sleep(0.1) + # Be sure to flush the buffer. + self._buffer_flush(event=None) + + + def do_calltip(self): + """ Analyse current and displays useful calltip for it. + """ + if self.debug: + print >>sys.__stdout__, "do_calltip" + separators = re.compile('[\s\{\}\[\]\(\)\= ,:]') + symbol = self.input_buffer + symbol_string = separators.split(symbol)[-1] + base_symbol_string = symbol_string.split('.')[0] + if base_symbol_string in self.shell.user_ns: + symbol = self.shell.user_ns[base_symbol_string] + elif base_symbol_string in self.shell.user_global_ns: + symbol = self.shell.user_global_ns[base_symbol_string] + elif base_symbol_string in __builtin__.__dict__: + symbol = __builtin__.__dict__[base_symbol_string] + else: + return False + try: + for name in symbol_string.split('.')[1:] + ['__doc__']: + symbol = getattr(symbol, name) + self.AutoCompCancel() + wx.Yield() + self.CallTipShow(self.GetCurrentPos(), symbol) + except: + # The retrieve symbol couldn't be converted to a string + pass + + + def _popup_completion(self, create=False): + """ 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.input_buffer + if (self.AutoCompActive() and not line[-1] == '.') \ + or create==True: + suggestion, completions = self.complete(line) + offset=0 + if completions: + complete_sep = re.compile('[\s\{\}\[\]\(\)\= ,:]') + residual = complete_sep.split(line)[-1] + offset = len(residual) + self.pop_completion(completions, offset=offset) + if self.debug: + print >>sys.__stdout__, completions + + + 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._out_buffer_lock.acquire() + self._out_buffer.append(text) + self._out_buffer_lock.release() + if not self._buffer_flush_timer.IsRunning(): + wx.CallAfter(self._buffer_flush_timer.Start, + milliseconds=100, oneShot=True) + + + #-------------------------------------------------------------------------- + # LineFrontEnd interface + #-------------------------------------------------------------------------- + + def execute(self, python_string, raw_string=None): + self._input_state = 'buffering' + self.CallTipCancel() + self._cursor = wx.BusyCursor() + if raw_string is None: + raw_string = python_string + end_line = self.current_prompt_line \ + + max(1, len(raw_string.split('\n'))-1) + for i in range(self.current_prompt_line, end_line): + 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) + + 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) + + + def release_output(self): + __builtin__.raw_input = self.__old_raw_input + PrefilterFrontEnd.release_output(self) + self.SetLexer(stc.STC_LEX_PYTHON) + + + def after_execute(self): + PrefilterFrontEnd.after_execute(self) + # Clear the wait cursor + if hasattr(self, '_cursor'): + del self._cursor + self.SetCursor(wx.StockCursor(wx.CURSOR_CHAR)) + + + def show_traceback(self): + start_line = self.GetCurrentLine() + PrefilterFrontEnd.show_traceback(self) + wx.Yield() + for i in range(start_line, self.GetCurrentLine()): + self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER) + + + #-------------------------------------------------------------------------- + # ConsoleWidget interface + #-------------------------------------------------------------------------- + + def new_prompt(self, prompt): + """ Display a new prompt, and start a new input buffer. + """ + self._input_state = 'readline' + ConsoleWidget.new_prompt(self, prompt) + i = self.current_prompt_line + self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER) + + + def write(self, *args, **kwargs): + # Avoid multiple inheritence, be explicit about which + # parent method class gets called + ConsoleWidget.write(self, *args, **kwargs) + + + def _on_key_down(self, event, skip=True): + """ Capture the character events, let the parent + widget handle them, and put our logic afterward. + """ + # FIXME: This method needs to be broken down in smaller ones. + current_line_number = self.GetCurrentLine() + if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown(): + # Capture Control-C + if self._input_state == 'subprocess': + if self.debug: + print >>sys.__stderr__, 'Killing running process' + self._running_process.process.kill() + elif self._input_state == 'buffering': + if self.debug: + print >>sys.__stderr__, 'Raising KeyboardInterrupt' + raise KeyboardInterrupt + # XXX: We need to make really sure we + # get back to a prompt. + elif self._input_state == 'subprocess' and ( + ( event.KeyCode<256 and + not event.ControlDown() ) + or + ( event.KeyCode in (ord('d'), ord('D')) and + event.ControlDown())): + # We are running a process, we redirect keys. + ConsoleWidget._on_key_down(self, event, skip=skip) + char = chr(event.KeyCode) + # Deal with some inconsistency in wx keycodes: + if char == '\r': + char = '\n' + elif not event.ShiftDown(): + char = char.lower() + if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')): + char = '\04' + self._running_process.process.stdin.write(char) + self._running_process.process.stdin.flush() + elif event.KeyCode in (ord('('), 57): + # Calltips + event.Skip() + self.do_calltip() + elif self.AutoCompActive() and not event.KeyCode == ord('\t'): + event.Skip() + if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE): + wx.CallAfter(self._popup_completion, create=True) + elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, + wx.WXK_RIGHT, wx.WXK_ESCAPE): + wx.CallAfter(self._popup_completion) + else: + # Up history + if event.KeyCode == wx.WXK_UP and ( + ( current_line_number == self.current_prompt_line and + event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) ) + or event.ControlDown() ): + new_buffer = self.get_history_previous( + self.input_buffer) + if new_buffer is not None: + self.input_buffer = new_buffer + if self.GetCurrentLine() > self.current_prompt_line: + # Go to first line, for seemless history up. + self.GotoPos(self.current_prompt_pos) + # Down history + elif event.KeyCode == wx.WXK_DOWN and ( + ( current_line_number == self.LineCount -1 and + event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) ) + or event.ControlDown() ): + new_buffer = self.get_history_next() + if new_buffer is not None: + 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): + self.complete_current_input() + if self.AutoCompActive(): + wx.CallAfter(self._popup_completion, create=True) + else: + event.Skip() + else: + ConsoleWidget._on_key_down(self, event, skip=skip) + + + def _on_key_up(self, event, skip=True): + """ Called when any key is released. + """ + if event.KeyCode in (59, ord('.')): + # Intercepting '.' + event.Skip() + self._popup_completion(create=True) + else: + ConsoleWidget._on_key_up(self, event, skip=skip) + + + def _on_enter(self): + """ Called on return key down, in readline input_state. + """ + if self.debug: + print >>sys.__stdout__, repr(self.input_buffer) + PrefilterFrontEnd._on_enter(self) + + + #-------------------------------------------------------------------------- + # EditWindow API + #-------------------------------------------------------------------------- + + def OnUpdateUI(self, event): + """ Override the OnUpdateUI of the EditWindow class, to prevent + syntax highlighting both for faster redraw, and for more + consistent look and feel. + """ + if not self._input_state == 'readline': + ConsoleWidget.OnUpdateUI(self, event) + + #-------------------------------------------------------------------------- + # 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. + + This is always called in the mainloop, by the wx timer. + """ + self._out_buffer_lock.acquire() + _out_buffer = self._out_buffer + self._out_buffer = [] + self._out_buffer_lock.release() + self.write(''.join(_out_buffer), refresh=False) + + + def _colorize_input_buffer(self): + """ Keep the input buffer lines at a bright color. + """ + if not self._input_state in ('readline', 'raw_input'): + return + end_line = self.GetCurrentLine() + if not sys.platform == 'win32': + end_line += 1 + for i in range(self.current_prompt_line, end_line): + if i in self._markers: + self.MarkerDeleteHandle(self._markers[i]) + self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER) + + +if __name__ == '__main__': + class MainWindow(wx.Frame): + def __init__(self, parent, id, title): + wx.Frame.__init__(self, parent, id, title, size=(300,250)) + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.shell = WxController(self) + self._sizer.Add(self.shell, 1, wx.EXPAND) + self.SetSizer(self._sizer) + self.SetAutoLayout(1) + self.Show(True) + + app = wx.PySimpleApp() + frame = MainWindow(None, wx.ID_ANY, 'Ipython') + frame.shell.SetFocus() + frame.SetSize((680, 460)) + self = frame.shell + + app.MainLoop() + diff --git a/IPython/frontend/zopeinterface.py b/IPython/frontend/zopeinterface.py new file mode 100644 index 0000000..fd37101 --- /dev/null +++ b/IPython/frontend/zopeinterface.py @@ -0,0 +1,34 @@ +# encoding: utf-8 +# -*- test-case-name: IPython.frontend.tests.test_frontendbase -*- +""" +zope.interface mock. If zope is installed, this module provides a zope +interface classes, if not it provides mocks for them. + +Classes provided: +Interface, Attribute, implements, classProvides +""" +__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 +#------------------------------------------------------------------------------- +import string +import uuid +import _ast + +try: + from zope.interface import Interface, Attribute, implements, classProvides +except ImportError: + #zope.interface is not available + Interface = object + def Attribute(name, doc): pass + def implements(interface): pass + def classProvides(interface): pass + diff --git a/IPython/iplib.py b/IPython/iplib.py index 9e17cc7..ed75b95 100644 --- a/IPython/iplib.py +++ b/IPython/iplib.py @@ -727,7 +727,7 @@ class InteractiveShell(object,Magic): batchrun = True # without -i option, exit after running the batch file if batchrun and not self.rc.interact: - self.exit_now = True + self.ask_exit() def add_builtins(self): """Store ipython references into the builtin namespace. @@ -1592,7 +1592,7 @@ want to merge them back into the new files.""" % locals() #sys.argv = ['-c'] self.push(self.prefilter(self.rc.c, False)) if not self.rc.interact: - self.exit_now = True + self.ask_exit() def embed_mainloop(self,header='',local_ns=None,global_ns=None,stack_depth=0): """Embeds IPython into a running python program. @@ -1755,7 +1755,8 @@ want to merge them back into the new files.""" % locals() if self.has_readline: self.readline_startup_hook(self.pre_readline) - # exit_now is set by a call to %Exit or %Quit + # exit_now is set by a call to %Exit or %Quit, through the + # ask_exit callback. while not self.exit_now: self.hooks.pre_prompt_hook() @@ -2151,7 +2152,7 @@ want to merge them back into the new files.""" % locals() except ValueError: warn("\n********\nYou or a %run:ed script called sys.stdin.close()" " or sys.stdout.close()!\nExiting IPython!") - self.exit_now = True + self.ask_exit() return "" # Try to be reasonably smart about not re-indenting pasted input more @@ -2502,16 +2503,20 @@ want to merge them back into the new files.""" % locals() """Write a string to the default error output""" Term.cerr.write(data) + def ask_exit(self): + """ Call for exiting. Can be overiden and used as a callback. """ + self.exit_now = True + def exit(self): """Handle interactive exit. - This method sets the exit_now attribute.""" + This method calls the ask_exit callback.""" if self.rc.confirm_exit: if self.ask_yes_no('Do you really want to exit ([y]/n)?','y'): - self.exit_now = True + self.ask_exit() else: - self.exit_now = True + self.ask_exit() def safe_execfile(self,fname,*where,**kw): """A safe version of the builtin execfile(). diff --git a/IPython/kernel/core/fd_redirector.py b/IPython/kernel/core/fd_redirector.py new file mode 100644 index 0000000..510a439 --- /dev/null +++ b/IPython/kernel/core/fd_redirector.py @@ -0,0 +1,81 @@ +# encoding: utf-8 + +""" +Stdout/stderr redirector, at the OS level, using file descriptors. + +This also works under windows. +""" + +__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. +#------------------------------------------------------------------------------- + + +import os +import sys + +STDOUT = 1 +STDERR = 2 + +class FDRedirector(object): + """ Class to redirect output (stdout or stderr) at the OS level using + file descriptors. + """ + + def __init__(self, fd=STDOUT): + """ fd is the file descriptor of the outpout you want to capture. + It can be STDOUT or STERR. + """ + self.fd = fd + self.started = False + self.piper = None + self.pipew = None + + def start(self): + """ Setup the redirection. + """ + if not self.started: + self.oldhandle = os.dup(self.fd) + self.piper, self.pipew = os.pipe() + os.dup2(self.pipew, self.fd) + os.close(self.pipew) + + self.started = True + + def flush(self): + """ Flush the captured output, similar to the flush method of any + stream. + """ + if self.fd == STDOUT: + sys.stdout.flush() + elif self.fd == STDERR: + sys.stderr.flush() + + def stop(self): + """ Unset the redirection and return the captured output. + """ + if self.started: + self.flush() + os.dup2(self.oldhandle, self.fd) + os.close(self.oldhandle) + f = os.fdopen(self.piper, 'r') + output = f.read() + f.close() + + self.started = False + return output + else: + return '' + + def getvalue(self): + """ Return the output captured since the last getvalue, or the + start of the redirection. + """ + output = self.stop() + self.start() + return output diff --git a/IPython/kernel/core/file_like.py b/IPython/kernel/core/file_like.py new file mode 100644 index 0000000..f984451 --- /dev/null +++ b/IPython/kernel/core/file_like.py @@ -0,0 +1,66 @@ +# encoding: utf-8 + +""" File like object that redirects its write calls to a given callback.""" + +__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. +#------------------------------------------------------------------------------- + +import sys + +class FileLike(object): + """ FileLike object that redirects all write to a callback. + + Only the write-related methods are implemented, as well as those + required to read a StringIO. + """ + closed = False + + def __init__(self, write_callback): + self.write = write_callback + + def flush(self): + """ This method is there for compatibility with other file-like + objects. + """ + pass + + def close(self): + """ This method is there for compatibility with other file-like + objects. + """ + pass + + def writelines(self, lines): + map(self.write, lines) + + def isatty(self): + """ This method is there for compatibility with other file-like + objects. + """ + return False + + def getvalue(self): + """ This method is there for compatibility with other file-like + objects. + """ + return '' + + def reset(self): + """ This method is there for compatibility with other file-like + objects. + """ + pass + + def truncate(self): + """ This method is there for compatibility with other file-like + objects. + """ + pass + + diff --git a/IPython/kernel/core/history.py b/IPython/kernel/core/history.py index 1baa029..736aac6 100644 --- a/IPython/kernel/core/history.py +++ b/IPython/kernel/core/history.py @@ -56,7 +56,7 @@ class History(object): """ Returns the history string at index, where index is the distance from the end (positive). """ - if index>0 and index=0 and index>out, r.getvalue(), + print >>out, i + except: + r.stop() + raise + r.stop() + assert out.getvalue() == "".join("%ic\n%i\n" %(i, i) for i in range(10)) + + +def test_redirector_output_trap(): + """ This test check not only that the redirector_output_trap does + trap the output, but also that it does it in a gready way, that + is by calling the callback ASAP. + """ + from IPython.kernel.core.redirector_output_trap import RedirectorOutputTrap + out = StringIO() + trap = RedirectorOutputTrap(out.write, out.write) + try: + trap.set() + for i in range(10): + os.system('echo %ic' % i) + print "%ip" % i + print >>out, i + except: + trap.unset() + raise + trap.unset() + assert out.getvalue() == "".join("%ic\n%ip\n%i\n" %(i, i, i) + for i in range(10)) + + + diff --git a/IPython/kernel/core/traceback_trap.py b/IPython/kernel/core/traceback_trap.py index 7fd1d17..6b1ad21 100644 --- a/IPython/kernel/core/traceback_trap.py +++ b/IPython/kernel/core/traceback_trap.py @@ -14,9 +14,8 @@ __docformat__ = "restructuredtext en" #------------------------------------------------------------------------------- # Imports #------------------------------------------------------------------------------- - import sys - +from traceback import format_list class TracebackTrap(object): """ Object to trap and format tracebacks. @@ -38,7 +37,6 @@ class TracebackTrap(object): def hook(self, *args): """ This method actually implements the hook. """ - self.args = args def set(self): @@ -76,8 +74,12 @@ class TracebackTrap(object): # Go through the list of formatters and let them add their formatting. traceback = {} - for formatter in self.formatters: - traceback[formatter.identifier] = formatter(*self.args) - + try: + for formatter in self.formatters: + traceback[formatter.identifier] = formatter(*self.args) + except: + # This works always, including with string exceptions. + traceback['fallback'] = repr(self.args) + message['traceback'] = traceback diff --git a/IPython/testing/decorators.py b/IPython/testing/decorators.py index c9f8832..1a07fe6 100644 --- a/IPython/testing/decorators.py +++ b/IPython/testing/decorators.py @@ -131,3 +131,5 @@ def skip(func): func.__name__) return apply_wrapper(wrapper,func) + + diff --git a/IPython/testing/plugin/Makefile b/IPython/testing/plugin/Makefile index 5586f40..da08e17 100644 --- a/IPython/testing/plugin/Makefile +++ b/IPython/testing/plugin/Makefile @@ -2,8 +2,8 @@ PREFIX=~/usr/local PREFIX=~/tmp/local -NOSE0=nosetests -vs --with-doctest --doctest-tests -NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt +NOSE0=nosetests -vs --with-doctest --doctest-tests --detailed-errors +NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt --detailed-errors SRC=ipdoctest.py setup.py ../decorators.py diff --git a/scripts/ipythonx b/scripts/ipythonx new file mode 100755 index 0000000..ef189c4 --- /dev/null +++ b/scripts/ipythonx @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""IPythonX -- An enhanced Interactive Python + +This script starts the Wx graphical frontend. This is experimental so +far. +""" + +from IPython.frontend.wx import ipythonx + +ipythonx.main() diff --git a/setup.py b/setup.py index fe79d38..98c1156 100755 --- a/setup.py +++ b/setup.py @@ -132,7 +132,8 @@ if 'setuptools' in sys.modules: 'pycolor = IPython.PyColorize:main', 'ipcontroller = IPython.kernel.scripts.ipcontroller:main', 'ipengine = IPython.kernel.scripts.ipengine:main', - 'ipcluster = IPython.kernel.scripts.ipcluster:main' + 'ipcluster = IPython.kernel.scripts.ipcluster:main', + 'ipythonx = IPython.frontend.wx.ipythonx:main' ] } setup_args["extras_require"] = dict( diff --git a/setupbase.py b/setupbase.py index 657596a..b5529ca 100644 --- a/setupbase.py +++ b/setupbase.py @@ -107,6 +107,10 @@ def find_packages(): add_package(packages, 'external') add_package(packages, 'gui') add_package(packages, 'gui.wx') + add_package(packages, 'frontend', tests=True) + add_package(packages, 'frontend._process') + add_package(packages, 'frontend.wx') + add_package(packages, 'frontend.cocoa', tests=True) add_package(packages, 'kernel', config=True, tests=True, scripts=True) add_package(packages, 'kernel.core', config=True, tests=True) add_package(packages, 'testing', tests=True) @@ -181,6 +185,7 @@ def find_scripts(): scripts.append('IPython/kernel/scripts/ipcontroller') scripts.append('IPython/kernel/scripts/ipcluster') scripts.append('scripts/ipython') + scripts.append('scripts/ipythonx') scripts.append('scripts/pycolor') scripts.append('scripts/irunner') @@ -229,4 +234,4 @@ def check_for_dependencies(): check_for_sphinx() check_for_pygments() check_for_nose() - check_for_pexpect() \ No newline at end of file + check_for_pexpect()