##// END OF EJS Templates
Replace all use of the ast module with the codeop module, and all use of...
Gael Varoquaux -
Show More
@@ -0,0 +1,155 b''
1 # encoding: utf-8
2
3 """This file contains unittests for the frontendbase module."""
4
5 __docformat__ = "restructuredtext en"
6
7 #---------------------------------------------------------------------------
8 # Copyright (C) 2008 The IPython Development Team
9 #
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
12 #---------------------------------------------------------------------------
13
14 #---------------------------------------------------------------------------
15 # Imports
16 #---------------------------------------------------------------------------
17
18 import unittest
19
20 try:
21 from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase
22 from IPython.frontend import frontendbase
23 from IPython.kernel.engineservice import EngineService
24 except ImportError:
25 import nose
26 raise nose.SkipTest("This test requires zope.interface, Twisted and Foolscap")
27
28 from IPython.testing.decorators import skip
29
30 class FrontEndCallbackChecker(AsyncFrontEndBase):
31 """FrontEndBase subclass for checking callbacks"""
32 def __init__(self, engine=None, history=None):
33 super(FrontEndCallbackChecker, self).__init__(engine=engine,
34 history=history)
35 self.updateCalled = False
36 self.renderResultCalled = False
37 self.renderErrorCalled = False
38
39 def update_cell_prompt(self, result, blockID=None):
40 self.updateCalled = True
41 return result
42
43 def render_result(self, result):
44 self.renderResultCalled = True
45 return result
46
47
48 def render_error(self, failure):
49 self.renderErrorCalled = True
50 return failure
51
52
53
54
55 class TestAsyncFrontendBase(unittest.TestCase):
56 def setUp(self):
57 """Setup the EngineService and FrontEndBase"""
58
59 self.fb = FrontEndCallbackChecker(engine=EngineService())
60
61 def test_implements_IFrontEnd(self):
62 assert(frontendbase.IFrontEnd.implementedBy(
63 AsyncFrontEndBase))
64
65 def test_is_complete_returns_False_for_incomplete_block(self):
66 """"""
67
68 block = """def test(a):"""
69
70 assert(self.fb.is_complete(block) == False)
71
72 def test_is_complete_returns_True_for_complete_block(self):
73 """"""
74
75 block = """def test(a): pass"""
76
77 assert(self.fb.is_complete(block))
78
79 block = """a=3"""
80
81 assert(self.fb.is_complete(block))
82
83 def test_blockID_added_to_result(self):
84 block = """3+3"""
85
86 d = self.fb.execute(block, blockID='TEST_ID')
87
88 d.addCallback(self.checkBlockID, expected='TEST_ID')
89
90 def test_blockID_added_to_failure(self):
91 block = "raise Exception()"
92
93 d = self.fb.execute(block,blockID='TEST_ID')
94 d.addErrback(self.checkFailureID, expected='TEST_ID')
95
96 def checkBlockID(self, result, expected=""):
97 assert(result['blockID'] == expected)
98
99
100 def checkFailureID(self, failure, expected=""):
101 assert(failure.blockID == expected)
102
103
104 def test_callbacks_added_to_execute(self):
105 """test that
106 update_cell_prompt
107 render_result
108
109 are added to execute request
110 """
111
112 d = self.fb.execute("10+10")
113 d.addCallback(self.checkCallbacks)
114
115 def checkCallbacks(self, result):
116 assert(self.fb.updateCalled)
117 assert(self.fb.renderResultCalled)
118
119 @skip("This test fails and lead to an unhandled error in a Deferred.")
120 def test_error_callback_added_to_execute(self):
121 """test that render_error called on execution error"""
122
123 d = self.fb.execute("raise Exception()")
124 d.addCallback(self.checkRenderError)
125
126 def checkRenderError(self, result):
127 assert(self.fb.renderErrorCalled)
128
129 def test_history_returns_expected_block(self):
130 """Make sure history browsing doesn't fail"""
131
132 blocks = ["a=1","a=2","a=3"]
133 for b in blocks:
134 d = self.fb.execute(b)
135
136 # d is now the deferred for the last executed block
137 d.addCallback(self.historyTests, blocks)
138
139
140 def historyTests(self, result, blocks):
141 """historyTests"""
142
143 assert(len(blocks) >= 3)
144 assert(self.fb.get_history_previous("") == blocks[-2])
145 assert(self.fb.get_history_previous("") == blocks[-3])
146 assert(self.fb.get_history_next() == blocks[-2])
147
148
149 def test_history_returns_none_at_startup(self):
150 """test_history_returns_none_at_startup"""
151
152 assert(self.fb.get_history_previous("")==None)
153 assert(self.fb.get_history_next()==None)
154
155
@@ -1,76 +1,76 b''
1 """
1 """
2 Base front end class for all async frontends.
2 Base front end class for all async frontends.
3 """
3 """
4 __docformat__ = "restructuredtext en"
4 __docformat__ = "restructuredtext en"
5
5
6 #-------------------------------------------------------------------------------
6 #-------------------------------------------------------------------------------
7 # Copyright (C) 2008 The IPython Development Team
7 # Copyright (C) 2008 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-------------------------------------------------------------------------------
11 #-------------------------------------------------------------------------------
12
12
13
13
14 #-------------------------------------------------------------------------------
14 #-------------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-------------------------------------------------------------------------------
16 #-------------------------------------------------------------------------------
17 import uuid
17 from IPython.external import guid
18
18
19
19
20 from zope.interface import Interface, Attribute, implements, classProvides
20 from zope.interface import Interface, Attribute, implements, classProvides
21 from twisted.python.failure import Failure
21 from twisted.python.failure import Failure
22 from IPython.frontend.frontendbase import FrontEndBase, IFrontEnd, IFrontEndFactory
22 from IPython.frontend.frontendbase import FrontEndBase, IFrontEnd, IFrontEndFactory
23 from IPython.kernel.core.history import FrontEndHistory
23 from IPython.kernel.core.history import FrontEndHistory
24 from IPython.kernel.engineservice import IEngineCore
24 from IPython.kernel.engineservice import IEngineCore
25
25
26
26
27 class AsyncFrontEndBase(FrontEndBase):
27 class AsyncFrontEndBase(FrontEndBase):
28 """
28 """
29 Overrides FrontEndBase to wrap execute in a deferred result.
29 Overrides FrontEndBase to wrap execute in a deferred result.
30 All callbacks are made as callbacks on the deferred result.
30 All callbacks are made as callbacks on the deferred result.
31 """
31 """
32
32
33 implements(IFrontEnd)
33 implements(IFrontEnd)
34 classProvides(IFrontEndFactory)
34 classProvides(IFrontEndFactory)
35
35
36 def __init__(self, engine=None, history=None):
36 def __init__(self, engine=None, history=None):
37 assert(engine==None or IEngineCore.providedBy(engine))
37 assert(engine==None or IEngineCore.providedBy(engine))
38 self.engine = IEngineCore(engine)
38 self.engine = IEngineCore(engine)
39 if history is None:
39 if history is None:
40 self.history = FrontEndHistory(input_cache=[''])
40 self.history = FrontEndHistory(input_cache=[''])
41 else:
41 else:
42 self.history = history
42 self.history = history
43
43
44
44
45 def execute(self, block, blockID=None):
45 def execute(self, block, blockID=None):
46 """Execute the block and return the deferred result.
46 """Execute the block and return the deferred result.
47
47
48 Parameters:
48 Parameters:
49 block : {str, AST}
49 block : {str, AST}
50 blockID : any
50 blockID : any
51 Caller may provide an ID to identify this block.
51 Caller may provide an ID to identify this block.
52 result['blockID'] := blockID
52 result['blockID'] := blockID
53
53
54 Result:
54 Result:
55 Deferred result of self.interpreter.execute
55 Deferred result of self.interpreter.execute
56 """
56 """
57
57
58 if(not self.is_complete(block)):
58 if(not self.is_complete(block)):
59 return Failure(Exception("Block is not compilable"))
59 return Failure(Exception("Block is not compilable"))
60
60
61 if(blockID == None):
61 if(blockID == None):
62 blockID = uuid.uuid4() #random UUID
62 blockID = guid.generate()
63
63
64 d = self.engine.execute(block)
64 d = self.engine.execute(block)
65 d.addCallback(self._add_history, block=block)
65 d.addCallback(self._add_history, block=block)
66 d.addCallbacks(self._add_block_id_for_result,
66 d.addCallbacks(self._add_block_id_for_result,
67 errback=self._add_block_id_for_failure,
67 errback=self._add_block_id_for_failure,
68 callbackArgs=(blockID,),
68 callbackArgs=(blockID,),
69 errbackArgs=(blockID,))
69 errbackArgs=(blockID,))
70 d.addBoth(self.update_cell_prompt, blockID=blockID)
70 d.addBoth(self.update_cell_prompt, blockID=blockID)
71 d.addCallbacks(self.render_result,
71 d.addCallbacks(self.render_result,
72 errback=self.render_error)
72 errback=self.render_error)
73
73
74 return d
74 return d
75
75
76
76
@@ -1,560 +1,560 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3
3
4 """PyObjC classes to provide a Cocoa frontend to the
4 """PyObjC classes to provide a Cocoa frontend to the
5 IPython.kernel.engineservice.IEngineBase.
5 IPython.kernel.engineservice.IEngineBase.
6
6
7 To add an IPython interpreter to a cocoa app, instantiate an
7 To add an IPython interpreter to a cocoa app, instantiate an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 NSTextView instance in your UI. That's it.
9 NSTextView instance in your UI. That's it.
10
10
11 Author: Barry Wark
11 Author: Barry Wark
12 """
12 """
13
13
14 __docformat__ = "restructuredtext en"
14 __docformat__ = "restructuredtext en"
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Copyright (C) 2008 The IPython Development Team
17 # Copyright (C) 2008 The IPython Development Team
18 #
18 #
19 # Distributed under the terms of the BSD License. The full license is in
19 # Distributed under the terms of the BSD License. The full license is in
20 # the file COPYING, distributed as part of this software.
20 # the file COPYING, distributed as part of this software.
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Imports
24 # Imports
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 import sys
27 import sys
28 import objc
28 import objc
29 import uuid
29 from IPython.external import guid
30
30
31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
32 NSLog, NSNotificationCenter, NSMakeRange,\
32 NSLog, NSNotificationCenter, NSMakeRange,\
33 NSLocalizedString, NSIntersectionRange,\
33 NSLocalizedString, NSIntersectionRange,\
34 NSString, NSAutoreleasePool
34 NSString, NSAutoreleasePool
35
35
36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
37 NSTextView, NSRulerView, NSVerticalRuler
37 NSTextView, NSRulerView, NSVerticalRuler
38
38
39 from pprint import saferepr
39 from pprint import saferepr
40
40
41 import IPython
41 import IPython
42 from IPython.kernel.engineservice import ThreadedEngineService
42 from IPython.kernel.engineservice import ThreadedEngineService
43 from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase
43 from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase
44
44
45 from twisted.internet.threads import blockingCallFromThread
45 from twisted.internet.threads import blockingCallFromThread
46 from twisted.python.failure import Failure
46 from twisted.python.failure import Failure
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Classes to implement the Cocoa frontend
49 # Classes to implement the Cocoa frontend
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 # TODO:
52 # TODO:
53 # 1. use MultiEngineClient and out-of-process engine rather than
53 # 1. use MultiEngineClient and out-of-process engine rather than
54 # ThreadedEngineService?
54 # ThreadedEngineService?
55 # 2. integrate Xgrid launching of engines
55 # 2. integrate Xgrid launching of engines
56
56
57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
58 """Wrap all blocks in an NSAutoreleasePool"""
58 """Wrap all blocks in an NSAutoreleasePool"""
59
59
60 def wrapped_execute(self, msg, lines):
60 def wrapped_execute(self, msg, lines):
61 """wrapped_execute"""
61 """wrapped_execute"""
62 try:
62 try:
63 p = NSAutoreleasePool.alloc().init()
63 p = NSAutoreleasePool.alloc().init()
64 result = super(AutoreleasePoolWrappedThreadedEngineService,
64 result = super(AutoreleasePoolWrappedThreadedEngineService,
65 self).wrapped_execute(msg, lines)
65 self).wrapped_execute(msg, lines)
66 finally:
66 finally:
67 p.drain()
67 p.drain()
68
68
69 return result
69 return result
70
70
71
71
72
72
73 class Cell(NSObject):
73 class Cell(NSObject):
74 """
74 """
75 Representation of the prompts, input and output of a cell in the
75 Representation of the prompts, input and output of a cell in the
76 frontend
76 frontend
77 """
77 """
78
78
79 blockNumber = objc.ivar().unsigned_long()
79 blockNumber = objc.ivar().unsigned_long()
80 blockID = objc.ivar()
80 blockID = objc.ivar()
81 inputBlock = objc.ivar()
81 inputBlock = objc.ivar()
82 output = objc.ivar()
82 output = objc.ivar()
83
83
84
84
85
85
86 class CellBlock(object):
86 class CellBlock(object):
87 """
87 """
88 Storage for information about text ranges relating to a single cell
88 Storage for information about text ranges relating to a single cell
89 """
89 """
90
90
91
91
92 def __init__(self, inputPromptRange, inputRange=None, outputPromptRange=None,
92 def __init__(self, inputPromptRange, inputRange=None, outputPromptRange=None,
93 outputRange=None):
93 outputRange=None):
94 super(CellBlock, self).__init__()
94 super(CellBlock, self).__init__()
95 self.inputPromptRange = inputPromptRange
95 self.inputPromptRange = inputPromptRange
96 self.inputRange = inputRange
96 self.inputRange = inputRange
97 self.outputPromptRange = outputPromptRange
97 self.outputPromptRange = outputPromptRange
98 self.outputRange = outputRange
98 self.outputRange = outputRange
99
99
100 def update_ranges_for_insertion(self, text, textRange):
100 def update_ranges_for_insertion(self, text, textRange):
101 """Update ranges for text insertion at textRange"""
101 """Update ranges for text insertion at textRange"""
102
102
103 for r in [self.inputPromptRange,self.inputRange,
103 for r in [self.inputPromptRange,self.inputRange,
104 self.outputPromptRange, self.outputRange]:
104 self.outputPromptRange, self.outputRange]:
105 if(r == None):
105 if(r == None):
106 continue
106 continue
107 intersection = NSIntersectionRange(r,textRange)
107 intersection = NSIntersectionRange(r,textRange)
108 if(intersection.length == 0): #ranges don't intersect
108 if(intersection.length == 0): #ranges don't intersect
109 if r.location >= textRange.location:
109 if r.location >= textRange.location:
110 r.location += len(text)
110 r.location += len(text)
111 else: #ranges intersect
111 else: #ranges intersect
112 if(r.location > textRange.location):
112 if(r.location > textRange.location):
113 offset = len(text) - intersection.length
113 offset = len(text) - intersection.length
114 r.length -= offset
114 r.length -= offset
115 r.location += offset
115 r.location += offset
116 elif(r.location == textRange.location):
116 elif(r.location == textRange.location):
117 r.length += len(text) - intersection.length
117 r.length += len(text) - intersection.length
118 else:
118 else:
119 r.length -= intersection.length
119 r.length -= intersection.length
120
120
121
121
122 def update_ranges_for_deletion(self, textRange):
122 def update_ranges_for_deletion(self, textRange):
123 """Update ranges for text deletion at textRange"""
123 """Update ranges for text deletion at textRange"""
124
124
125 for r in [self.inputPromptRange,self.inputRange,
125 for r in [self.inputPromptRange,self.inputRange,
126 self.outputPromptRange, self.outputRange]:
126 self.outputPromptRange, self.outputRange]:
127 if(r==None):
127 if(r==None):
128 continue
128 continue
129 intersection = NSIntersectionRange(r, textRange)
129 intersection = NSIntersectionRange(r, textRange)
130 if(intersection.length == 0): #ranges don't intersect
130 if(intersection.length == 0): #ranges don't intersect
131 if r.location >= textRange.location:
131 if r.location >= textRange.location:
132 r.location -= textRange.length
132 r.location -= textRange.length
133 else: #ranges intersect
133 else: #ranges intersect
134 if(r.location > textRange.location):
134 if(r.location > textRange.location):
135 offset = intersection.length
135 offset = intersection.length
136 r.length -= offset
136 r.length -= offset
137 r.location += offset
137 r.location += offset
138 elif(r.location == textRange.location):
138 elif(r.location == textRange.location):
139 r.length += intersection.length
139 r.length += intersection.length
140 else:
140 else:
141 r.length -= intersection.length
141 r.length -= intersection.length
142
142
143 def __repr__(self):
143 def __repr__(self):
144 return 'CellBlock('+ str((self.inputPromptRange,
144 return 'CellBlock('+ str((self.inputPromptRange,
145 self.inputRange,
145 self.inputRange,
146 self.outputPromptRange,
146 self.outputPromptRange,
147 self.outputRange)) + ')'
147 self.outputRange)) + ')'
148
148
149
149
150
150
151
151
152 class IPythonCocoaController(NSObject, AsyncFrontEndBase):
152 class IPythonCocoaController(NSObject, AsyncFrontEndBase):
153 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
153 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
154 waitingForEngine = objc.ivar().bool()
154 waitingForEngine = objc.ivar().bool()
155 textView = objc.IBOutlet()
155 textView = objc.IBOutlet()
156
156
157 def init(self):
157 def init(self):
158 self = super(IPythonCocoaController, self).init()
158 self = super(IPythonCocoaController, self).init()
159 AsyncFrontEndBase.__init__(self,
159 AsyncFrontEndBase.__init__(self,
160 engine=AutoreleasePoolWrappedThreadedEngineService())
160 engine=AutoreleasePoolWrappedThreadedEngineService())
161 if(self != None):
161 if(self != None):
162 self._common_init()
162 self._common_init()
163
163
164 return self
164 return self
165
165
166 def _common_init(self):
166 def _common_init(self):
167 """_common_init"""
167 """_common_init"""
168
168
169 self.userNS = NSMutableDictionary.dictionary()
169 self.userNS = NSMutableDictionary.dictionary()
170 self.waitingForEngine = False
170 self.waitingForEngine = False
171
171
172 self.lines = {}
172 self.lines = {}
173 self.tabSpaces = 4
173 self.tabSpaces = 4
174 self.tabUsesSpaces = True
174 self.tabUsesSpaces = True
175 self.currentBlockID = self.next_block_ID()
175 self.currentBlockID = self.next_block_ID()
176 self.blockRanges = {} # blockID=>CellBlock
176 self.blockRanges = {} # blockID=>CellBlock
177
177
178
178
179 def awakeFromNib(self):
179 def awakeFromNib(self):
180 """awakeFromNib"""
180 """awakeFromNib"""
181
181
182 self._common_init()
182 self._common_init()
183
183
184 # Start the IPython engine
184 # Start the IPython engine
185 self.engine.startService()
185 self.engine.startService()
186 NSLog('IPython engine started')
186 NSLog('IPython engine started')
187
187
188 # Register for app termination
188 # Register for app termination
189 nc = NSNotificationCenter.defaultCenter()
189 nc = NSNotificationCenter.defaultCenter()
190 nc.addObserver_selector_name_object_(
190 nc.addObserver_selector_name_object_(
191 self,
191 self,
192 'appWillTerminate:',
192 'appWillTerminate:',
193 NSApplicationWillTerminateNotification,
193 NSApplicationWillTerminateNotification,
194 None)
194 None)
195
195
196 self.textView.setDelegate_(self)
196 self.textView.setDelegate_(self)
197 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
197 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
198 r = NSRulerView.alloc().initWithScrollView_orientation_(
198 r = NSRulerView.alloc().initWithScrollView_orientation_(
199 self.textView.enclosingScrollView(),
199 self.textView.enclosingScrollView(),
200 NSVerticalRuler)
200 NSVerticalRuler)
201 self.verticalRulerView = r
201 self.verticalRulerView = r
202 self.verticalRulerView.setClientView_(self.textView)
202 self.verticalRulerView.setClientView_(self.textView)
203 self._start_cli_banner()
203 self._start_cli_banner()
204 self.start_new_block()
204 self.start_new_block()
205
205
206
206
207 def appWillTerminate_(self, notification):
207 def appWillTerminate_(self, notification):
208 """appWillTerminate"""
208 """appWillTerminate"""
209
209
210 self.engine.stopService()
210 self.engine.stopService()
211
211
212
212
213 def complete(self, token):
213 def complete(self, token):
214 """Complete token in engine's user_ns
214 """Complete token in engine's user_ns
215
215
216 Parameters
216 Parameters
217 ----------
217 ----------
218 token : string
218 token : string
219
219
220 Result
220 Result
221 ------
221 ------
222 Deferred result of
222 Deferred result of
223 IPython.kernel.engineservice.IEngineBase.complete
223 IPython.kernel.engineservice.IEngineBase.complete
224 """
224 """
225
225
226 return self.engine.complete(token)
226 return self.engine.complete(token)
227
227
228
228
229 def execute(self, block, blockID=None):
229 def execute(self, block, blockID=None):
230 self.waitingForEngine = True
230 self.waitingForEngine = True
231 self.willChangeValueForKey_('commandHistory')
231 self.willChangeValueForKey_('commandHistory')
232 d = super(IPythonCocoaController, self).execute(block,
232 d = super(IPythonCocoaController, self).execute(block,
233 blockID)
233 blockID)
234 d.addBoth(self._engine_done)
234 d.addBoth(self._engine_done)
235 d.addCallback(self._update_user_ns)
235 d.addCallback(self._update_user_ns)
236
236
237 return d
237 return d
238
238
239
239
240 def push_(self, namespace):
240 def push_(self, namespace):
241 """Push dictionary of key=>values to python namespace"""
241 """Push dictionary of key=>values to python namespace"""
242
242
243 self.waitingForEngine = True
243 self.waitingForEngine = True
244 self.willChangeValueForKey_('commandHistory')
244 self.willChangeValueForKey_('commandHistory')
245 d = self.engine.push(namespace)
245 d = self.engine.push(namespace)
246 d.addBoth(self._engine_done)
246 d.addBoth(self._engine_done)
247 d.addCallback(self._update_user_ns)
247 d.addCallback(self._update_user_ns)
248
248
249
249
250 def pull_(self, keys):
250 def pull_(self, keys):
251 """Pull keys from python namespace"""
251 """Pull keys from python namespace"""
252
252
253 self.waitingForEngine = True
253 self.waitingForEngine = True
254 result = blockingCallFromThread(self.engine.pull, keys)
254 result = blockingCallFromThread(self.engine.pull, keys)
255 self.waitingForEngine = False
255 self.waitingForEngine = False
256
256
257 @objc.signature('v@:@I')
257 @objc.signature('v@:@I')
258 def executeFileAtPath_encoding_(self, path, encoding):
258 def executeFileAtPath_encoding_(self, path, encoding):
259 """Execute file at path in an empty namespace. Update the engine
259 """Execute file at path in an empty namespace. Update the engine
260 user_ns with the resulting locals."""
260 user_ns with the resulting locals."""
261
261
262 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
262 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
263 path,
263 path,
264 encoding,
264 encoding,
265 None)
265 None)
266 self.engine.execute(lines)
266 self.engine.execute(lines)
267
267
268
268
269 def _engine_done(self, x):
269 def _engine_done(self, x):
270 self.waitingForEngine = False
270 self.waitingForEngine = False
271 self.didChangeValueForKey_('commandHistory')
271 self.didChangeValueForKey_('commandHistory')
272 return x
272 return x
273
273
274 def _update_user_ns(self, result):
274 def _update_user_ns(self, result):
275 """Update self.userNS from self.engine's namespace"""
275 """Update self.userNS from self.engine's namespace"""
276 d = self.engine.keys()
276 d = self.engine.keys()
277 d.addCallback(self._get_engine_namespace_values_for_keys)
277 d.addCallback(self._get_engine_namespace_values_for_keys)
278
278
279 return result
279 return result
280
280
281
281
282 def _get_engine_namespace_values_for_keys(self, keys):
282 def _get_engine_namespace_values_for_keys(self, keys):
283 d = self.engine.pull(keys)
283 d = self.engine.pull(keys)
284 d.addCallback(self._store_engine_namespace_values, keys=keys)
284 d.addCallback(self._store_engine_namespace_values, keys=keys)
285
285
286
286
287 def _store_engine_namespace_values(self, values, keys=[]):
287 def _store_engine_namespace_values(self, values, keys=[]):
288 assert(len(values) == len(keys))
288 assert(len(values) == len(keys))
289 self.willChangeValueForKey_('userNS')
289 self.willChangeValueForKey_('userNS')
290 for (k,v) in zip(keys,values):
290 for (k,v) in zip(keys,values):
291 self.userNS[k] = saferepr(v)
291 self.userNS[k] = saferepr(v)
292 self.didChangeValueForKey_('userNS')
292 self.didChangeValueForKey_('userNS')
293
293
294
294
295 def update_cell_prompt(self, result, blockID=None):
295 def update_cell_prompt(self, result, blockID=None):
296 print self.blockRanges
296 print self.blockRanges
297 if(isinstance(result, Failure)):
297 if(isinstance(result, Failure)):
298 prompt = self.input_prompt()
298 prompt = self.input_prompt()
299
299
300 else:
300 else:
301 prompt = self.input_prompt(number=result['number'])
301 prompt = self.input_prompt(number=result['number'])
302
302
303 r = self.blockRanges[blockID].inputPromptRange
303 r = self.blockRanges[blockID].inputPromptRange
304 self.insert_text(prompt,
304 self.insert_text(prompt,
305 textRange=r,
305 textRange=r,
306 scrollToVisible=False
306 scrollToVisible=False
307 )
307 )
308
308
309 return result
309 return result
310
310
311
311
312 def render_result(self, result):
312 def render_result(self, result):
313 blockID = result['blockID']
313 blockID = result['blockID']
314 inputRange = self.blockRanges[blockID].inputRange
314 inputRange = self.blockRanges[blockID].inputRange
315 del self.blockRanges[blockID]
315 del self.blockRanges[blockID]
316
316
317 #print inputRange,self.current_block_range()
317 #print inputRange,self.current_block_range()
318 self.insert_text('\n' +
318 self.insert_text('\n' +
319 self.output_prompt(number=result['number']) +
319 self.output_prompt(number=result['number']) +
320 result.get('display',{}).get('pprint','') +
320 result.get('display',{}).get('pprint','') +
321 '\n\n',
321 '\n\n',
322 textRange=NSMakeRange(inputRange.location+inputRange.length,
322 textRange=NSMakeRange(inputRange.location+inputRange.length,
323 0))
323 0))
324 return result
324 return result
325
325
326
326
327 def render_error(self, failure):
327 def render_error(self, failure):
328 print failure
328 print failure
329 blockID = failure.blockID
329 blockID = failure.blockID
330 inputRange = self.blockRanges[blockID].inputRange
330 inputRange = self.blockRanges[blockID].inputRange
331 self.insert_text('\n' +
331 self.insert_text('\n' +
332 self.output_prompt() +
332 self.output_prompt() +
333 '\n' +
333 '\n' +
334 failure.getErrorMessage() +
334 failure.getErrorMessage() +
335 '\n\n',
335 '\n\n',
336 textRange=NSMakeRange(inputRange.location +
336 textRange=NSMakeRange(inputRange.location +
337 inputRange.length,
337 inputRange.length,
338 0))
338 0))
339 self.start_new_block()
339 self.start_new_block()
340 return failure
340 return failure
341
341
342
342
343 def _start_cli_banner(self):
343 def _start_cli_banner(self):
344 """Print banner"""
344 """Print banner"""
345
345
346 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
346 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
347 IPython.__version__
347 IPython.__version__
348
348
349 self.insert_text(banner + '\n\n')
349 self.insert_text(banner + '\n\n')
350
350
351
351
352 def start_new_block(self):
352 def start_new_block(self):
353 """"""
353 """"""
354
354
355 self.currentBlockID = self.next_block_ID()
355 self.currentBlockID = self.next_block_ID()
356 self.blockRanges[self.currentBlockID] = self.new_cell_block()
356 self.blockRanges[self.currentBlockID] = self.new_cell_block()
357 self.insert_text(self.input_prompt(),
357 self.insert_text(self.input_prompt(),
358 textRange=self.current_block_range().inputPromptRange)
358 textRange=self.current_block_range().inputPromptRange)
359
359
360
360
361
361
362 def next_block_ID(self):
362 def next_block_ID(self):
363
363
364 return uuid.uuid4()
364 return guid.generate()
365
365
366 def new_cell_block(self):
366 def new_cell_block(self):
367 """A new CellBlock at the end of self.textView.textStorage()"""
367 """A new CellBlock at the end of self.textView.textStorage()"""
368
368
369 return CellBlock(NSMakeRange(self.textView.textStorage().length(),
369 return CellBlock(NSMakeRange(self.textView.textStorage().length(),
370 0), #len(self.input_prompt())),
370 0), #len(self.input_prompt())),
371 NSMakeRange(self.textView.textStorage().length(),# + len(self.input_prompt()),
371 NSMakeRange(self.textView.textStorage().length(),# + len(self.input_prompt()),
372 0))
372 0))
373
373
374
374
375 def current_block_range(self):
375 def current_block_range(self):
376 return self.blockRanges.get(self.currentBlockID,
376 return self.blockRanges.get(self.currentBlockID,
377 self.new_cell_block())
377 self.new_cell_block())
378
378
379 def current_block(self):
379 def current_block(self):
380 """The current block's text"""
380 """The current block's text"""
381
381
382 return self.text_for_range(self.current_block_range().inputRange)
382 return self.text_for_range(self.current_block_range().inputRange)
383
383
384 def text_for_range(self, textRange):
384 def text_for_range(self, textRange):
385 """text_for_range"""
385 """text_for_range"""
386
386
387 ts = self.textView.textStorage()
387 ts = self.textView.textStorage()
388 return ts.string().substringWithRange_(textRange)
388 return ts.string().substringWithRange_(textRange)
389
389
390 def current_line(self):
390 def current_line(self):
391 block = self.text_for_range(self.current_block_range().inputRange)
391 block = self.text_for_range(self.current_block_range().inputRange)
392 block = block.split('\n')
392 block = block.split('\n')
393 return block[-1]
393 return block[-1]
394
394
395
395
396 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
396 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
397 """Insert text into textView at textRange, updating blockRanges
397 """Insert text into textView at textRange, updating blockRanges
398 as necessary
398 as necessary
399 """
399 """
400 if(textRange == None):
400 if(textRange == None):
401 #range for end of text
401 #range for end of text
402 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
402 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
403
403
404
404
405 self.textView.replaceCharactersInRange_withString_(
405 self.textView.replaceCharactersInRange_withString_(
406 textRange, string)
406 textRange, string)
407
407
408 for r in self.blockRanges.itervalues():
408 for r in self.blockRanges.itervalues():
409 r.update_ranges_for_insertion(string, textRange)
409 r.update_ranges_for_insertion(string, textRange)
410
410
411 self.textView.setSelectedRange_(textRange)
411 self.textView.setSelectedRange_(textRange)
412 if(scrollToVisible):
412 if(scrollToVisible):
413 self.textView.scrollRangeToVisible_(textRange)
413 self.textView.scrollRangeToVisible_(textRange)
414
414
415
415
416
416
417 def replace_current_block_with_string(self, textView, string):
417 def replace_current_block_with_string(self, textView, string):
418 textView.replaceCharactersInRange_withString_(
418 textView.replaceCharactersInRange_withString_(
419 self.current_block_range().inputRange,
419 self.current_block_range().inputRange,
420 string)
420 string)
421 self.current_block_range().inputRange.length = len(string)
421 self.current_block_range().inputRange.length = len(string)
422 r = NSMakeRange(textView.textStorage().length(), 0)
422 r = NSMakeRange(textView.textStorage().length(), 0)
423 textView.scrollRangeToVisible_(r)
423 textView.scrollRangeToVisible_(r)
424 textView.setSelectedRange_(r)
424 textView.setSelectedRange_(r)
425
425
426
426
427 def current_indent_string(self):
427 def current_indent_string(self):
428 """returns string for indent or None if no indent"""
428 """returns string for indent or None if no indent"""
429
429
430 return self._indent_for_block(self.current_block())
430 return self._indent_for_block(self.current_block())
431
431
432
432
433 def _indent_for_block(self, block):
433 def _indent_for_block(self, block):
434 lines = block.split('\n')
434 lines = block.split('\n')
435 if(len(lines) > 1):
435 if(len(lines) > 1):
436 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
436 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
437 if(currentIndent == 0):
437 if(currentIndent == 0):
438 currentIndent = self.tabSpaces
438 currentIndent = self.tabSpaces
439
439
440 if(self.tabUsesSpaces):
440 if(self.tabUsesSpaces):
441 result = ' ' * currentIndent
441 result = ' ' * currentIndent
442 else:
442 else:
443 result = '\t' * (currentIndent/self.tabSpaces)
443 result = '\t' * (currentIndent/self.tabSpaces)
444 else:
444 else:
445 result = None
445 result = None
446
446
447 return result
447 return result
448
448
449
449
450 # NSTextView delegate methods...
450 # NSTextView delegate methods...
451 def textView_doCommandBySelector_(self, textView, selector):
451 def textView_doCommandBySelector_(self, textView, selector):
452 assert(textView == self.textView)
452 assert(textView == self.textView)
453 NSLog("textView_doCommandBySelector_: "+selector)
453 NSLog("textView_doCommandBySelector_: "+selector)
454
454
455
455
456 if(selector == 'insertNewline:'):
456 if(selector == 'insertNewline:'):
457 indent = self.current_indent_string()
457 indent = self.current_indent_string()
458 if(indent):
458 if(indent):
459 line = indent + self.current_line()
459 line = indent + self.current_line()
460 else:
460 else:
461 line = self.current_line()
461 line = self.current_line()
462
462
463 if(self.is_complete(self.current_block())):
463 if(self.is_complete(self.current_block())):
464 self.execute(self.current_block(),
464 self.execute(self.current_block(),
465 blockID=self.currentBlockID)
465 blockID=self.currentBlockID)
466 self.start_new_block()
466 self.start_new_block()
467
467
468 return True
468 return True
469
469
470 return False
470 return False
471
471
472 elif(selector == 'moveUp:'):
472 elif(selector == 'moveUp:'):
473 prevBlock = self.get_history_previous(self.current_block())
473 prevBlock = self.get_history_previous(self.current_block())
474 if(prevBlock != None):
474 if(prevBlock != None):
475 self.replace_current_block_with_string(textView, prevBlock)
475 self.replace_current_block_with_string(textView, prevBlock)
476 else:
476 else:
477 NSBeep()
477 NSBeep()
478 return True
478 return True
479
479
480 elif(selector == 'moveDown:'):
480 elif(selector == 'moveDown:'):
481 nextBlock = self.get_history_next()
481 nextBlock = self.get_history_next()
482 if(nextBlock != None):
482 if(nextBlock != None):
483 self.replace_current_block_with_string(textView, nextBlock)
483 self.replace_current_block_with_string(textView, nextBlock)
484 else:
484 else:
485 NSBeep()
485 NSBeep()
486 return True
486 return True
487
487
488 elif(selector == 'moveToBeginningOfParagraph:'):
488 elif(selector == 'moveToBeginningOfParagraph:'):
489 textView.setSelectedRange_(NSMakeRange(
489 textView.setSelectedRange_(NSMakeRange(
490 self.current_block_range().inputRange.location,
490 self.current_block_range().inputRange.location,
491 0))
491 0))
492 return True
492 return True
493 elif(selector == 'moveToEndOfParagraph:'):
493 elif(selector == 'moveToEndOfParagraph:'):
494 textView.setSelectedRange_(NSMakeRange(
494 textView.setSelectedRange_(NSMakeRange(
495 self.current_block_range().inputRange.location + \
495 self.current_block_range().inputRange.location + \
496 self.current_block_range().inputRange.length, 0))
496 self.current_block_range().inputRange.length, 0))
497 return True
497 return True
498 elif(selector == 'deleteToEndOfParagraph:'):
498 elif(selector == 'deleteToEndOfParagraph:'):
499 if(textView.selectedRange().location <= \
499 if(textView.selectedRange().location <= \
500 self.current_block_range().location):
500 self.current_block_range().location):
501 raise NotImplemented()
501 raise NotImplemented()
502
502
503 return False # don't actually handle the delete
503 return False # don't actually handle the delete
504
504
505 elif(selector == 'insertTab:'):
505 elif(selector == 'insertTab:'):
506 if(len(self.current_line().strip()) == 0): #only white space
506 if(len(self.current_line().strip()) == 0): #only white space
507 return False
507 return False
508 else:
508 else:
509 self.textView.complete_(self)
509 self.textView.complete_(self)
510 return True
510 return True
511
511
512 elif(selector == 'deleteBackward:'):
512 elif(selector == 'deleteBackward:'):
513 #if we're at the beginning of the current block, ignore
513 #if we're at the beginning of the current block, ignore
514 if(textView.selectedRange().location == \
514 if(textView.selectedRange().location == \
515 self.current_block_range().inputRange.location):
515 self.current_block_range().inputRange.location):
516 return True
516 return True
517 else:
517 else:
518 for r in self.blockRanges.itervalues():
518 for r in self.blockRanges.itervalues():
519 deleteRange = textView.selectedRange
519 deleteRange = textView.selectedRange
520 if(deleteRange.length == 0):
520 if(deleteRange.length == 0):
521 deleteRange.location -= 1
521 deleteRange.location -= 1
522 deleteRange.length = 1
522 deleteRange.length = 1
523 r.update_ranges_for_deletion(deleteRange)
523 r.update_ranges_for_deletion(deleteRange)
524 return False
524 return False
525 return False
525 return False
526
526
527
527
528 def textView_shouldChangeTextInRanges_replacementStrings_(self,
528 def textView_shouldChangeTextInRanges_replacementStrings_(self,
529 textView, ranges, replacementStrings):
529 textView, ranges, replacementStrings):
530 """
530 """
531 Delegate method for NSTextView.
531 Delegate method for NSTextView.
532
532
533 Refuse change text in ranges not at end, but make those changes at
533 Refuse change text in ranges not at end, but make those changes at
534 end.
534 end.
535 """
535 """
536
536
537 assert(len(ranges) == len(replacementStrings))
537 assert(len(ranges) == len(replacementStrings))
538 allow = True
538 allow = True
539 for r,s in zip(ranges, replacementStrings):
539 for r,s in zip(ranges, replacementStrings):
540 r = r.rangeValue()
540 r = r.rangeValue()
541 if(textView.textStorage().length() > 0 and
541 if(textView.textStorage().length() > 0 and
542 r.location < self.current_block_range().inputRange.location):
542 r.location < self.current_block_range().inputRange.location):
543 self.insert_text(s)
543 self.insert_text(s)
544 allow = False
544 allow = False
545
545
546 return allow
546 return allow
547
547
548 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
548 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
549 textView, words, charRange, index):
549 textView, words, charRange, index):
550 try:
550 try:
551 ts = textView.textStorage()
551 ts = textView.textStorage()
552 token = ts.string().substringWithRange_(charRange)
552 token = ts.string().substringWithRange_(charRange)
553 completions = blockingCallFromThread(self.complete, token)
553 completions = blockingCallFromThread(self.complete, token)
554 except:
554 except:
555 completions = objc.nil
555 completions = objc.nil
556 NSBeep()
556 NSBeep()
557
557
558 return (completions,0)
558 return (completions,0)
559
559
560
560
@@ -1,429 +1,343 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
3 """
3 """
4 frontendbase provides an interface and base class for GUI frontends for
4 frontendbase provides an interface and base class for GUI frontends for
5 IPython.kernel/IPython.kernel.core.
5 IPython.kernel/IPython.kernel.core.
6
6
7 Frontend implementations will likely want to subclass FrontEndBase.
7 Frontend implementations will likely want to subclass FrontEndBase.
8
8
9 Author: Barry Wark
9 Author: Barry Wark
10 """
10 """
11 __docformat__ = "restructuredtext en"
11 __docformat__ = "restructuredtext en"
12
12
13 #-------------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
14 # Copyright (C) 2008 The IPython Development Team
14 # Copyright (C) 2008 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-------------------------------------------------------------------------------
18 #-------------------------------------------------------------------------------
19
19
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-------------------------------------------------------------------------------
22 #-------------------------------------------------------------------------------
23 import string
23 import string
24
24 import codeop
25 try:
25 from IPython.external import guid
26 import _ast
27 except ImportError:
28 # Python 2.4 hackish workaround.
29 class bunch: pass
30 _ast = bunch()
31 _ast.PyCF_ONLY_AST = 1024
32
33
34
35 try:
36 import uuid
37 except ImportError:
38 # Python 2.4 hackish workaround.
39 class UUID:
40 def __init__(self,bytes):
41 version = 4
42 int = long(('%02x'*16) % tuple(map(ord, bytes)), 16)
43 # Set the variant to RFC 4122.
44 int &= ~(0xc000 << 48L)
45 int |= 0x8000 << 48L
46 # Set the version number.
47 int &= ~(0xf000 << 64L)
48 int |= version << 76L
49 self.__dict__['int'] = int
50
51 def __cmp__(self, other):
52 if isinstance(other, UUID):
53 return cmp(self.int, other.int)
54 return NotImplemented
55
56 def __hash__(self):
57 return hash(self.int)
58
59 def __int__(self):
60 return self.int
61
62 def __repr__(self):
63 return 'UUID(%r)' % str(self)
64
65 def __setattr__(self, name, value):
66 raise TypeError('UUID objects are immutable')
67
68 def __str__(self):
69 hex = '%032x' % self.int
70 return '%s-%s-%s-%s-%s' % (
71 hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:])
72
73 def get_bytes(self):
74 bytes = ''
75 for shift in range(0, 128, 8):
76 bytes = chr((self.int >> shift) & 0xff) + bytes
77 return bytes
78
79 bytes = property(get_bytes)
80
81
82 def _u4():
83 "Fake random uuid"
84
85 import random
86 bytes = [chr(random.randrange(256)) for i in range(16)]
87 return UUID(bytes)
88
89 class bunch: pass
90 uuid = bunch()
91 uuid.uuid4 = _u4
92 del _u4
93
94
26
95
27
96 from IPython.frontend.zopeinterface import (
28 from IPython.frontend.zopeinterface import (
97 Interface,
29 Interface,
98 Attribute,
30 Attribute,
99 )
31 )
100 from IPython.kernel.core.history import FrontEndHistory
32 from IPython.kernel.core.history import FrontEndHistory
101 from IPython.kernel.core.util import Bunch
33 from IPython.kernel.core.util import Bunch
102
34
103 ##############################################################################
35 ##############################################################################
104 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
36 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
105 # not
37 # not
106
38
107 rc = Bunch()
39 rc = Bunch()
108 rc.prompt_in1 = r'In [$number]: '
40 rc.prompt_in1 = r'In [$number]: '
109 rc.prompt_in2 = r'...'
41 rc.prompt_in2 = r'...'
110 rc.prompt_out = r'Out [$number]: '
42 rc.prompt_out = r'Out [$number]: '
111
43
112 ##############################################################################
44 ##############################################################################
113 # Interface definitions
45 # Interface definitions
114 ##############################################################################
46 ##############################################################################
115
47
116 class IFrontEndFactory(Interface):
48 class IFrontEndFactory(Interface):
117 """Factory interface for frontends."""
49 """Factory interface for frontends."""
118
50
119 def __call__(engine=None, history=None):
51 def __call__(engine=None, history=None):
120 """
52 """
121 Parameters:
53 Parameters:
122 interpreter : IPython.kernel.engineservice.IEngineCore
54 interpreter : IPython.kernel.engineservice.IEngineCore
123 """
55 """
124
56
125 pass
57 pass
126
58
127
59
128 class IFrontEnd(Interface):
60 class IFrontEnd(Interface):
129 """Interface for frontends. All methods return t.i.d.Deferred"""
61 """Interface for frontends. All methods return t.i.d.Deferred"""
130
62
131 Attribute("input_prompt_template", "string.Template instance\
63 Attribute("input_prompt_template", "string.Template instance\
132 substituteable with execute result.")
64 substituteable with execute result.")
133 Attribute("output_prompt_template", "string.Template instance\
65 Attribute("output_prompt_template", "string.Template instance\
134 substituteable with execute result.")
66 substituteable with execute result.")
135 Attribute("continuation_prompt_template", "string.Template instance\
67 Attribute("continuation_prompt_template", "string.Template instance\
136 substituteable with execute result.")
68 substituteable with execute result.")
137
69
138 def update_cell_prompt(result, blockID=None):
70 def update_cell_prompt(result, blockID=None):
139 """Subclass may override to update the input prompt for a block.
71 """Subclass may override to update the input prompt for a block.
140
72
141 In asynchronous frontends, this method will be called as a
73 In asynchronous frontends, this method will be called as a
142 twisted.internet.defer.Deferred's callback/errback.
74 twisted.internet.defer.Deferred's callback/errback.
143 Implementations should thus return result when finished.
75 Implementations should thus return result when finished.
144
76
145 Result is a result dict in case of success, and a
77 Result is a result dict in case of success, and a
146 twisted.python.util.failure.Failure in case of an error
78 twisted.python.util.failure.Failure in case of an error
147 """
79 """
148
80
149 pass
81 pass
150
82
151 def render_result(result):
83 def render_result(result):
152 """Render the result of an execute call. Implementors may choose the
84 """Render the result of an execute call. Implementors may choose the
153 method of rendering.
85 method of rendering.
154 For example, a notebook-style frontend might render a Chaco plot
86 For example, a notebook-style frontend might render a Chaco plot
155 inline.
87 inline.
156
88
157 Parameters:
89 Parameters:
158 result : dict (result of IEngineBase.execute )
90 result : dict (result of IEngineBase.execute )
159 blockID = result['blockID']
91 blockID = result['blockID']
160
92
161 Result:
93 Result:
162 Output of frontend rendering
94 Output of frontend rendering
163 """
95 """
164
96
165 pass
97 pass
166
98
167 def render_error(failure):
99 def render_error(failure):
168 """Subclasses must override to render the failure.
100 """Subclasses must override to render the failure.
169
101
170 In asynchronous frontend, since this method will be called as a
102 In asynchronous frontend, since this method will be called as a
171 twisted.internet.defer.Deferred's callback. Implementations
103 twisted.internet.defer.Deferred's callback. Implementations
172 should thus return result when finished.
104 should thus return result when finished.
173
105
174 blockID = failure.blockID
106 blockID = failure.blockID
175 """
107 """
176
108
177 pass
109 pass
178
110
179 def input_prompt(number=''):
111 def input_prompt(number=''):
180 """Returns the input prompt by subsituting into
112 """Returns the input prompt by subsituting into
181 self.input_prompt_template
113 self.input_prompt_template
182 """
114 """
183 pass
115 pass
184
116
185 def output_prompt(number=''):
117 def output_prompt(number=''):
186 """Returns the output prompt by subsituting into
118 """Returns the output prompt by subsituting into
187 self.output_prompt_template
119 self.output_prompt_template
188 """
120 """
189
121
190 pass
122 pass
191
123
192 def continuation_prompt():
124 def continuation_prompt():
193 """Returns the continuation prompt by subsituting into
125 """Returns the continuation prompt by subsituting into
194 self.continuation_prompt_template
126 self.continuation_prompt_template
195 """
127 """
196
128
197 pass
129 pass
198
130
199 def is_complete(block):
131 def is_complete(block):
200 """Returns True if block is complete, False otherwise."""
132 """Returns True if block is complete, False otherwise."""
201
133
202 pass
134 pass
203
135
204 def compile_ast(block):
136
205 """Compiles block to an _ast.AST"""
206
207 pass
208
209 def get_history_previous(current_block):
137 def get_history_previous(current_block):
210 """Returns the block previous in the history. Saves currentBlock if
138 """Returns the block previous in the history. Saves currentBlock if
211 the history_cursor is currently at the end of the input history"""
139 the history_cursor is currently at the end of the input history"""
212 pass
140 pass
213
141
214 def get_history_next():
142 def get_history_next():
215 """Returns the next block in the history."""
143 """Returns the next block in the history."""
216
144
217 pass
145 pass
218
146
219 def complete(self, line):
147 def complete(self, line):
220 """Returns the list of possible completions, and the completed
148 """Returns the list of possible completions, and the completed
221 line.
149 line.
222
150
223 The input argument is the full line to be completed. This method
151 The input argument is the full line to be completed. This method
224 returns both the line completed as much as possible, and the list
152 returns both the line completed as much as possible, and the list
225 of further possible completions (full words).
153 of further possible completions (full words).
226 """
154 """
227 pass
155 pass
228
156
229
157
230 ##############################################################################
158 ##############################################################################
231 # Base class for all the frontends.
159 # Base class for all the frontends.
232 ##############################################################################
160 ##############################################################################
233
161
234 class FrontEndBase(object):
162 class FrontEndBase(object):
235 """
163 """
236 FrontEndBase manages the state tasks for a CLI frontend:
164 FrontEndBase manages the state tasks for a CLI frontend:
237 - Input and output history management
165 - Input and output history management
238 - Input/continuation and output prompt generation
166 - Input/continuation and output prompt generation
239
167
240 Some issues (due to possibly unavailable engine):
168 Some issues (due to possibly unavailable engine):
241 - How do we get the current cell number for the engine?
169 - How do we get the current cell number for the engine?
242 - How do we handle completions?
170 - How do we handle completions?
243 """
171 """
244
172
245 history_cursor = 0
173 history_cursor = 0
246
174
247 input_prompt_template = string.Template(rc.prompt_in1)
175 input_prompt_template = string.Template(rc.prompt_in1)
248 output_prompt_template = string.Template(rc.prompt_out)
176 output_prompt_template = string.Template(rc.prompt_out)
249 continuation_prompt_template = string.Template(rc.prompt_in2)
177 continuation_prompt_template = string.Template(rc.prompt_in2)
250
178
251 def __init__(self, shell=None, history=None):
179 def __init__(self, shell=None, history=None):
252 self.shell = shell
180 self.shell = shell
253 if history is None:
181 if history is None:
254 self.history = FrontEndHistory(input_cache=[''])
182 self.history = FrontEndHistory(input_cache=[''])
255 else:
183 else:
256 self.history = history
184 self.history = history
257
185
258
186
259 def input_prompt(self, number=''):
187 def input_prompt(self, number=''):
260 """Returns the current input prompt
188 """Returns the current input prompt
261
189
262 It would be great to use ipython1.core.prompts.Prompt1 here
190 It would be great to use ipython1.core.prompts.Prompt1 here
263 """
191 """
264 return self.input_prompt_template.safe_substitute({'number':number})
192 return self.input_prompt_template.safe_substitute({'number':number})
265
193
266
194
267 def continuation_prompt(self):
195 def continuation_prompt(self):
268 """Returns the current continuation prompt"""
196 """Returns the current continuation prompt"""
269
197
270 return self.continuation_prompt_template.safe_substitute()
198 return self.continuation_prompt_template.safe_substitute()
271
199
272 def output_prompt(self, number=''):
200 def output_prompt(self, number=''):
273 """Returns the output prompt for result"""
201 """Returns the output prompt for result"""
274
202
275 return self.output_prompt_template.safe_substitute({'number':number})
203 return self.output_prompt_template.safe_substitute({'number':number})
276
204
277
205
278 def is_complete(self, block):
206 def is_complete(self, block):
279 """Determine if block is complete.
207 """Determine if block is complete.
280
208
281 Parameters
209 Parameters
282 block : string
210 block : string
283
211
284 Result
212 Result
285 True if block can be sent to the engine without compile errors.
213 True if block can be sent to the engine without compile errors.
286 False otherwise.
214 False otherwise.
287 """
215 """
288
216
289 try:
217 try:
290 ast = self.compile_ast(block)
218 is_complete = codeop.compile_command(block.rstrip() + '\n\n',
219 "<string>", "exec")
291 except:
220 except:
292 return False
221 return False
293
222
294 lines = block.split('\n')
223 lines = block.split('\n')
295 return (len(lines)==1 or str(lines[-1])=='')
224 return ((is_complete is not None)
296
225 and (len(lines)==1 or str(lines[-1])==''))
297
298 def compile_ast(self, block):
299 """Compile block to an AST
300
301 Parameters:
302 block : str
303
304 Result:
305 AST
306
307 Throws:
308 Exception if block cannot be compiled
309 """
310
311 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
312
226
313
227
314 def execute(self, block, blockID=None):
228 def execute(self, block, blockID=None):
315 """Execute the block and return the result.
229 """Execute the block and return the result.
316
230
317 Parameters:
231 Parameters:
318 block : {str, AST}
232 block : {str, AST}
319 blockID : any
233 blockID : any
320 Caller may provide an ID to identify this block.
234 Caller may provide an ID to identify this block.
321 result['blockID'] := blockID
235 result['blockID'] := blockID
322
236
323 Result:
237 Result:
324 Deferred result of self.interpreter.execute
238 Deferred result of self.interpreter.execute
325 """
239 """
326
240
327 if(not self.is_complete(block)):
241 if(not self.is_complete(block)):
328 raise Exception("Block is not compilable")
242 raise Exception("Block is not compilable")
329
243
330 if(blockID == None):
244 if(blockID == None):
331 blockID = uuid.uuid4() #random UUID
245 blockID = guid.generate()
332
246
333 try:
247 try:
334 result = self.shell.execute(block)
248 result = self.shell.execute(block)
335 except Exception,e:
249 except Exception,e:
336 e = self._add_block_id_for_failure(e, blockID=blockID)
250 e = self._add_block_id_for_failure(e, blockID=blockID)
337 e = self.update_cell_prompt(e, blockID=blockID)
251 e = self.update_cell_prompt(e, blockID=blockID)
338 e = self.render_error(e)
252 e = self.render_error(e)
339 else:
253 else:
340 result = self._add_block_id_for_result(result, blockID=blockID)
254 result = self._add_block_id_for_result(result, blockID=blockID)
341 result = self.update_cell_prompt(result, blockID=blockID)
255 result = self.update_cell_prompt(result, blockID=blockID)
342 result = self.render_result(result)
256 result = self.render_result(result)
343
257
344 return result
258 return result
345
259
346
260
347 def _add_block_id_for_result(self, result, blockID):
261 def _add_block_id_for_result(self, result, blockID):
348 """Add the blockID to result or failure. Unfortunatley, we have to
262 """Add the blockID to result or failure. Unfortunatley, we have to
349 treat failures differently than result dicts.
263 treat failures differently than result dicts.
350 """
264 """
351
265
352 result['blockID'] = blockID
266 result['blockID'] = blockID
353
267
354 return result
268 return result
355
269
356 def _add_block_id_for_failure(self, failure, blockID):
270 def _add_block_id_for_failure(self, failure, blockID):
357 """_add_block_id_for_failure"""
271 """_add_block_id_for_failure"""
358 failure.blockID = blockID
272 failure.blockID = blockID
359 return failure
273 return failure
360
274
361
275
362 def _add_history(self, result, block=None):
276 def _add_history(self, result, block=None):
363 """Add block to the history"""
277 """Add block to the history"""
364
278
365 assert(block != None)
279 assert(block != None)
366 self.history.add_items([block])
280 self.history.add_items([block])
367 self.history_cursor += 1
281 self.history_cursor += 1
368
282
369 return result
283 return result
370
284
371
285
372 def get_history_previous(self, current_block):
286 def get_history_previous(self, current_block):
373 """ Returns previous history string and decrement history cursor.
287 """ Returns previous history string and decrement history cursor.
374 """
288 """
375 command = self.history.get_history_item(self.history_cursor - 1)
289 command = self.history.get_history_item(self.history_cursor - 1)
376
290
377 if command is not None:
291 if command is not None:
378 if(self.history_cursor+1 == len(self.history.input_cache)):
292 if(self.history_cursor+1 == len(self.history.input_cache)):
379 self.history.input_cache[self.history_cursor] = current_block
293 self.history.input_cache[self.history_cursor] = current_block
380 self.history_cursor -= 1
294 self.history_cursor -= 1
381 return command
295 return command
382
296
383
297
384 def get_history_next(self):
298 def get_history_next(self):
385 """ Returns next history string and increment history cursor.
299 """ Returns next history string and increment history cursor.
386 """
300 """
387 command = self.history.get_history_item(self.history_cursor+1)
301 command = self.history.get_history_item(self.history_cursor+1)
388
302
389 if command is not None:
303 if command is not None:
390 self.history_cursor += 1
304 self.history_cursor += 1
391 return command
305 return command
392
306
393 ###
307 ###
394 # Subclasses probably want to override these methods...
308 # Subclasses probably want to override these methods...
395 ###
309 ###
396
310
397 def update_cell_prompt(self, result, blockID=None):
311 def update_cell_prompt(self, result, blockID=None):
398 """Subclass may override to update the input prompt for a block.
312 """Subclass may override to update the input prompt for a block.
399
313
400 This method only really makes sens in asyncrhonous frontend.
314 This method only really makes sens in asyncrhonous frontend.
401 Since this method will be called as a
315 Since this method will be called as a
402 twisted.internet.defer.Deferred's callback, implementations should
316 twisted.internet.defer.Deferred's callback, implementations should
403 return result when finished.
317 return result when finished.
404 """
318 """
405
319
406 raise NotImplementedError
320 raise NotImplementedError
407
321
408
322
409 def render_result(self, result):
323 def render_result(self, result):
410 """Subclasses must override to render result.
324 """Subclasses must override to render result.
411
325
412 In asynchronous frontends, this method will be called as a
326 In asynchronous frontends, this method will be called as a
413 twisted.internet.defer.Deferred's callback. Implementations
327 twisted.internet.defer.Deferred's callback. Implementations
414 should thus return result when finished.
328 should thus return result when finished.
415 """
329 """
416
330
417 raise NotImplementedError
331 raise NotImplementedError
418
332
419
333
420 def render_error(self, failure):
334 def render_error(self, failure):
421 """Subclasses must override to render the failure.
335 """Subclasses must override to render the failure.
422
336
423 In asynchronous frontends, this method will be called as a
337 In asynchronous frontends, this method will be called as a
424 twisted.internet.defer.Deferred's callback. Implementations
338 twisted.internet.defer.Deferred's callback. Implementations
425 should thus return result when finished.
339 should thus return result when finished.
426 """
340 """
427
341
428 raise NotImplementedError
342 raise NotImplementedError
429
343
@@ -1,155 +1,32 b''
1 # encoding: utf-8
1 # encoding: utf-8
2
2 """
3 """This file contains unittests for the frontendbase module."""
3 Test the basic functionality of frontendbase.
4 """
4
5
5 __docformat__ = "restructuredtext en"
6 __docformat__ = "restructuredtext en"
6
7
7 #---------------------------------------------------------------------------
8 #-------------------------------------------------------------------------------
8 # Copyright (C) 2008 The IPython Development Team
9 # Copyright (C) 2008 The IPython Development Team
9 #
10 #
10 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is
11 # the file COPYING, distributed as part of this software.
12 # in the file COPYING, distributed as part of this software.
12 #---------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
13
14
14 #---------------------------------------------------------------------------
15 from IPython.frontend.frontendbase import FrontEndBase
15 # Imports
16
16 #---------------------------------------------------------------------------
17 def test_iscomplete():
17
18 """ Check that is_complete works.
18 import unittest
19 """
19
20 f = FrontEndBase()
20 try:
21 assert f.is_complete('(a + a)')
21 from IPython.frontend.asyncfrontendbase import AsyncFrontEndBase
22 assert not f.is_complete('(a + a')
22 from IPython.frontend import frontendbase
23 assert f.is_complete('1')
23 from IPython.kernel.engineservice import EngineService
24 assert not f.is_complete('1 + ')
24 except ImportError:
25 assert not f.is_complete('1 + \n\n')
25 import nose
26 assert f.is_complete('if True:\n print 1\n')
26 raise nose.SkipTest("This test requires zope.interface, Twisted and Foolscap")
27 assert not f.is_complete('if True:\n print 1')
27
28 assert f.is_complete('def f():\n print 1\n')
28 from IPython.testing.decorators import skip
29
29
30 if __name__ == '__main__':
30 class FrontEndCallbackChecker(AsyncFrontEndBase):
31 test_iscomplete()
31 """FrontEndBase subclass for checking callbacks"""
32 def __init__(self, engine=None, history=None):
33 super(FrontEndCallbackChecker, self).__init__(engine=engine,
34 history=history)
35 self.updateCalled = False
36 self.renderResultCalled = False
37 self.renderErrorCalled = False
38
39 def update_cell_prompt(self, result, blockID=None):
40 self.updateCalled = True
41 return result
42
43 def render_result(self, result):
44 self.renderResultCalled = True
45 return result
46
47
48 def render_error(self, failure):
49 self.renderErrorCalled = True
50 return failure
51
52
53
32
54
55 class TestAsyncFrontendBase(unittest.TestCase):
56 def setUp(self):
57 """Setup the EngineService and FrontEndBase"""
58
59 self.fb = FrontEndCallbackChecker(engine=EngineService())
60
61 def test_implements_IFrontEnd(self):
62 assert(frontendbase.IFrontEnd.implementedBy(
63 AsyncFrontEndBase))
64
65 def test_is_complete_returns_False_for_incomplete_block(self):
66 """"""
67
68 block = """def test(a):"""
69
70 assert(self.fb.is_complete(block) == False)
71
72 def test_is_complete_returns_True_for_complete_block(self):
73 """"""
74
75 block = """def test(a): pass"""
76
77 assert(self.fb.is_complete(block))
78
79 block = """a=3"""
80
81 assert(self.fb.is_complete(block))
82
83 def test_blockID_added_to_result(self):
84 block = """3+3"""
85
86 d = self.fb.execute(block, blockID='TEST_ID')
87
88 d.addCallback(self.checkBlockID, expected='TEST_ID')
89
90 def test_blockID_added_to_failure(self):
91 block = "raise Exception()"
92
93 d = self.fb.execute(block,blockID='TEST_ID')
94 d.addErrback(self.checkFailureID, expected='TEST_ID')
95
96 def checkBlockID(self, result, expected=""):
97 assert(result['blockID'] == expected)
98
99
100 def checkFailureID(self, failure, expected=""):
101 assert(failure.blockID == expected)
102
103
104 def test_callbacks_added_to_execute(self):
105 """test that
106 update_cell_prompt
107 render_result
108
109 are added to execute request
110 """
111
112 d = self.fb.execute("10+10")
113 d.addCallback(self.checkCallbacks)
114
115 def checkCallbacks(self, result):
116 assert(self.fb.updateCalled)
117 assert(self.fb.renderResultCalled)
118
119 @skip("This test fails and lead to an unhandled error in a Deferred.")
120 def test_error_callback_added_to_execute(self):
121 """test that render_error called on execution error"""
122
123 d = self.fb.execute("raise Exception()")
124 d.addCallback(self.checkRenderError)
125
126 def checkRenderError(self, result):
127 assert(self.fb.renderErrorCalled)
128
129 def test_history_returns_expected_block(self):
130 """Make sure history browsing doesn't fail"""
131
132 blocks = ["a=1","a=2","a=3"]
133 for b in blocks:
134 d = self.fb.execute(b)
135
136 # d is now the deferred for the last executed block
137 d.addCallback(self.historyTests, blocks)
138
139
140 def historyTests(self, result, blocks):
141 """historyTests"""
142
143 assert(len(blocks) >= 3)
144 assert(self.fb.get_history_previous("") == blocks[-2])
145 assert(self.fb.get_history_previous("") == blocks[-3])
146 assert(self.fb.get_history_next() == blocks[-2])
147
148
149 def test_history_returns_none_at_startup(self):
150 """test_history_returns_none_at_startup"""
151
152 assert(self.fb.get_history_previous("")==None)
153 assert(self.fb.get_history_next()==None)
154
155
General Comments 0
You need to be logged in to leave comments. Login now