##// END OF EJS Templates
added blockID for failures (special case)
Barry Wark -
Show More
@@ -1,389 +1,395 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: ipython1.frontend.cocoa.tests.test_cocoa_frontend -*-
2 # -*- test-case-name: ipython1.frontend.cocoa.tests.test_cocoa_frontend -*-
3
3
4 """PyObjC classes to provide a Cocoa frontend to the ipython1.kernel.engineservice.EngineService.
4 """PyObjC classes to provide a Cocoa frontend to the ipython1.kernel.engineservice.EngineService.
5
5
6 The Cocoa frontend is divided into two classes:
6 The Cocoa frontend is divided into two classes:
7 - IPythonCocoaController
7 - IPythonCocoaController
8 - IPythonCLITextViewDelegate
8 - IPythonCLITextViewDelegate
9
9
10 To add an IPython interpreter to a cocoa app, instantiate both of these classes in an XIB...[FINISH]
10 To add an IPython interpreter to a cocoa app, instantiate both of these classes in an XIB...[FINISH]
11 """
11 """
12
12
13 __docformat__ = "restructuredtext en"
13 __docformat__ = "restructuredtext en"
14
14
15 #-------------------------------------------------------------------------------
15 #-------------------------------------------------------------------------------
16 # Copyright (C) 2008 Barry Wark <barrywark@gmail.com>
16 # Copyright (C) 2008 Barry Wark <barrywark@gmail.com>
17 #
17 #
18 # Distributed under the terms of the BSD License. The full license is in
18 # Distributed under the terms of the BSD License. The full license is in
19 # the file COPYING, distributed as part of this software.
19 # the file COPYING, distributed as part of this software.
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21
21
22 #-------------------------------------------------------------------------------
22 #-------------------------------------------------------------------------------
23 # Imports
23 # Imports
24 #-------------------------------------------------------------------------------
24 #-------------------------------------------------------------------------------
25
25
26 import objc
26 import objc
27 import uuid
27 import uuid
28
28
29 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
29 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
30 NSLog, NSNotificationCenter, NSMakeRange,\
30 NSLog, NSNotificationCenter, NSMakeRange,\
31 NSLocalizedString, NSIntersectionRange
31 NSLocalizedString, NSIntersectionRange
32
32
33 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
33 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
34 NSTextView, NSRulerView, NSVerticalRuler
34 NSTextView, NSRulerView, NSVerticalRuler
35
35
36 from pprint import saferepr
36 from pprint import saferepr
37
37
38 import IPython
38 import IPython
39 from IPython.kernel.engineservice import EngineService, ThreadedEngineService
39 from IPython.kernel.engineservice import EngineService, ThreadedEngineService
40 from IPython.frontend.frontendbase import FrontEndBase
40 from IPython.frontend.frontendbase import FrontEndBase
41
41
42 from twisted.internet.threads import blockingCallFromThread
42 from twisted.internet.threads import blockingCallFromThread
43 from twisted.python.failure import Failure
43
44
44 #-------------------------------------------------------------------------------
45 #-------------------------------------------------------------------------------
45 # Classes to implement the Cocoa frontend
46 # Classes to implement the Cocoa frontend
46 #-------------------------------------------------------------------------------
47 #-------------------------------------------------------------------------------
47
48
48 # TODO:
49 # TODO:
49 # 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService?
50 # 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService?
50 # 2. integrate Xgrid launching of engines
51 # 2. integrate Xgrid launching of engines
51
52
52
53
53
54
54
55
55 class IPythonCocoaController(NSObject, FrontEndBase):
56 class IPythonCocoaController(NSObject, FrontEndBase):
56 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
57 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
57 waitingForEngine = objc.ivar().bool()
58 waitingForEngine = objc.ivar().bool()
58 textView = objc.IBOutlet()
59 textView = objc.IBOutlet()
59
60
60 def init(self):
61 def init(self):
61 self = super(IPythonCocoaController, self).init()
62 self = super(IPythonCocoaController, self).init()
62 FrontEndBase.__init__(self, engine=ThreadedEngineService())
63 FrontEndBase.__init__(self, engine=ThreadedEngineService())
63 if(self != None):
64 if(self != None):
64 self._common_init()
65 self._common_init()
65
66
66 return self
67 return self
67
68
68 def _common_init(self):
69 def _common_init(self):
69 """_common_init"""
70 """_common_init"""
70
71
71 self.userNS = NSMutableDictionary.dictionary()
72 self.userNS = NSMutableDictionary.dictionary()
72 self.waitingForEngine = False
73 self.waitingForEngine = False
73
74
74 self.lines = {}
75 self.lines = {}
75 self.tabSpaces = 4
76 self.tabSpaces = 4
76 self.tabUsesSpaces = True
77 self.tabUsesSpaces = True
77 self.currentBlockID = self.nextBlockID()
78 self.currentBlockID = self.nextBlockID()
78 self.blockRanges = {} # blockID=>NSRange
79 self.blockRanges = {} # blockID=>NSRange
79
80
80
81
81 def awakeFromNib(self):
82 def awakeFromNib(self):
82 """awakeFromNib"""
83 """awakeFromNib"""
83
84
84 self._common_init()
85 self._common_init()
85
86
86 # Start the IPython engine
87 # Start the IPython engine
87 self.engine.startService()
88 self.engine.startService()
88 NSLog('IPython engine started')
89 NSLog('IPython engine started')
89
90
90 # Register for app termination
91 # Register for app termination
91 NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self,
92 NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self,
92 'appWillTerminate:',
93 'appWillTerminate:',
93 NSApplicationWillTerminateNotification,
94 NSApplicationWillTerminateNotification,
94 None)
95 None)
95
96
96 self.textView.setDelegate_(self)
97 self.textView.setDelegate_(self)
97 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
98 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
98 self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_(
99 self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_(
99 self.textView.enclosingScrollView(),
100 self.textView.enclosingScrollView(),
100 NSVerticalRuler)
101 NSVerticalRuler)
101 self.verticalRulerView.setClientView_(self.textView)
102 self.verticalRulerView.setClientView_(self.textView)
102 self.startCLIForTextView()
103 self.startCLIForTextView()
103
104
104
105
105 def appWillTerminate_(self, notification):
106 def appWillTerminate_(self, notification):
106 """appWillTerminate"""
107 """appWillTerminate"""
107
108
108 self.engine.stopService()
109 self.engine.stopService()
109
110
110
111
111 def complete(self, token):
112 def complete(self, token):
112 """Complete token in engine's user_ns
113 """Complete token in engine's user_ns
113
114
114 Parameters
115 Parameters
115 ----------
116 ----------
116 token : string
117 token : string
117
118
118 Result
119 Result
119 ------
120 ------
120 Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete
121 Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete
121 """
122 """
122
123
123 return self.engine.complete(token)
124 return self.engine.complete(token)
124
125
125
126
126 def execute(self, block, blockID=None):
127 def execute(self, block, blockID=None):
127 self.waitingForEngine = True
128 self.waitingForEngine = True
128 self.willChangeValueForKey_('commandHistory')
129 self.willChangeValueForKey_('commandHistory')
129 d = super(IPythonCocoaController, self).execute(block, blockID)
130 d = super(IPythonCocoaController, self).execute(block, blockID)
130 d.addBoth(self._engineDone)
131 d.addBoth(self._engineDone)
131 d.addCallback(self._updateUserNS)
132 d.addCallback(self._updateUserNS)
132
133
133 return d
134 return d
134
135
135
136
136 def _engineDone(self, x):
137 def _engineDone(self, x):
137 self.waitingForEngine = False
138 self.waitingForEngine = False
138 self.didChangeValueForKey_('commandHistory')
139 self.didChangeValueForKey_('commandHistory')
139 return x
140 return x
140
141
141 def _updateUserNS(self, result):
142 def _updateUserNS(self, result):
142 """Update self.userNS from self.engine's namespace"""
143 """Update self.userNS from self.engine's namespace"""
143 d = self.engine.keys()
144 d = self.engine.keys()
144 d.addCallback(self._getEngineNamepsaceValuesForKeys)
145 d.addCallback(self._getEngineNamepsaceValuesForKeys)
145
146
146 return result
147 return result
147
148
148
149
149 def _getEngineNamepsaceValuesForKeys(self, keys):
150 def _getEngineNamepsaceValuesForKeys(self, keys):
150 d = self.engine.pull(keys)
151 d = self.engine.pull(keys)
151 d.addCallback(self._storeEngineNamespaceValues, keys=keys)
152 d.addCallback(self._storeEngineNamespaceValues, keys=keys)
152
153
153
154
154 def _storeEngineNamespaceValues(self, values, keys=[]):
155 def _storeEngineNamespaceValues(self, values, keys=[]):
155 assert(len(values) == len(keys))
156 assert(len(values) == len(keys))
156 self.willChangeValueForKey_('userNS')
157 self.willChangeValueForKey_('userNS')
157 for (k,v) in zip(keys,values):
158 for (k,v) in zip(keys,values):
158 self.userNS[k] = saferepr(v)
159 self.userNS[k] = saferepr(v)
159 self.didChangeValueForKey_('userNS')
160 self.didChangeValueForKey_('userNS')
160
161
161
162
162 def startCLIForTextView(self):
163 def startCLIForTextView(self):
163 """Print banner"""
164 """Print banner"""
164
165
165 banner = """IPython1 %s -- An enhanced Interactive Python.""" % IPython.__version__
166 banner = """IPython1 %s -- An enhanced Interactive Python.""" % IPython.__version__
166
167
167 self.insert_text(banner + '\n\n')
168 self.insert_text(banner + '\n\n')
168
169
169 # NSTextView/IPythonTextView delegate methods
170 # NSTextView/IPythonTextView delegate methods
170 def textView_doCommandBySelector_(self, textView, selector):
171 def textView_doCommandBySelector_(self, textView, selector):
171 assert(textView == self.textView)
172 assert(textView == self.textView)
172 NSLog("textView_doCommandBySelector_: "+selector)
173 NSLog("textView_doCommandBySelector_: "+selector)
173
174
174
175
175 if(selector == 'insertNewline:'):
176 if(selector == 'insertNewline:'):
176 indent = self.currentIndentString()
177 indent = self.currentIndentString()
177 if(indent):
178 if(indent):
178 line = indent + self.currentLine()
179 line = indent + self.currentLine()
179 else:
180 else:
180 line = self.currentLine()
181 line = self.currentLine()
181
182
182 if(self.is_complete(self.currentBlock())):
183 if(self.is_complete(self.currentBlock())):
183 self.execute(self.currentBlock(),
184 self.execute(self.currentBlock(),
184 blockID=self.currentBlockID)
185 blockID=self.currentBlockID)
185 self.startNewBlock()
186 self.startNewBlock()
186
187
187 return True
188 return True
188
189
189 return False
190 return False
190
191
191 elif(selector == 'moveUp:'):
192 elif(selector == 'moveUp:'):
192 prevBlock = self.get_history_previous(self.currentBlock())
193 prevBlock = self.get_history_previous(self.currentBlock())
193 if(prevBlock != None):
194 if(prevBlock != None):
194 self.replaceCurrentBlockWithString(textView, prevBlock)
195 self.replaceCurrentBlockWithString(textView, prevBlock)
195 else:
196 else:
196 NSBeep()
197 NSBeep()
197 return True
198 return True
198
199
199 elif(selector == 'moveDown:'):
200 elif(selector == 'moveDown:'):
200 nextBlock = self.get_history_next()
201 nextBlock = self.get_history_next()
201 if(nextBlock != None):
202 if(nextBlock != None):
202 self.replaceCurrentBlockWithString(textView, nextBlock)
203 self.replaceCurrentBlockWithString(textView, nextBlock)
203 else:
204 else:
204 NSBeep()
205 NSBeep()
205 return True
206 return True
206
207
207 elif(selector == 'moveToBeginningOfParagraph:'):
208 elif(selector == 'moveToBeginningOfParagraph:'):
208 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0))
209 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0))
209 return True
210 return True
210 elif(selector == 'moveToEndOfParagraph:'):
211 elif(selector == 'moveToEndOfParagraph:'):
211 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \
212 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \
212 self.currentBlockRange().length, 0))
213 self.currentBlockRange().length, 0))
213 return True
214 return True
214 elif(selector == 'deleteToEndOfParagraph:'):
215 elif(selector == 'deleteToEndOfParagraph:'):
215 if(textView.selectedRange().location <= self.currentBlockRange().location):
216 if(textView.selectedRange().location <= self.currentBlockRange().location):
216 # Intersect the selected range with the current line range
217 # Intersect the selected range with the current line range
217 if(self.currentBlockRange().length < 0):
218 if(self.currentBlockRange().length < 0):
218 self.blockRanges[self.currentBlockID].length = 0
219 self.blockRanges[self.currentBlockID].length = 0
219
220
220 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
221 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
221 self.currentBlockRange())
222 self.currentBlockRange())
222
223
223 if(r.length > 0): #no intersection
224 if(r.length > 0): #no intersection
224 textView.setSelectedRange_(r)
225 textView.setSelectedRange_(r)
225
226
226 return False # don't actually handle the delete
227 return False # don't actually handle the delete
227
228
228 elif(selector == 'insertTab:'):
229 elif(selector == 'insertTab:'):
229 if(len(self.currentLine().strip()) == 0): #only white space
230 if(len(self.currentLine().strip()) == 0): #only white space
230 return False
231 return False
231 else:
232 else:
232 self.textView.complete_(self)
233 self.textView.complete_(self)
233 return True
234 return True
234
235
235 elif(selector == 'deleteBackward:'):
236 elif(selector == 'deleteBackward:'):
236 #if we're at the beginning of the current block, ignore
237 #if we're at the beginning of the current block, ignore
237 if(textView.selectedRange().location == self.currentBlockRange().location):
238 if(textView.selectedRange().location == self.currentBlockRange().location):
238 return True
239 return True
239 else:
240 else:
240 self.currentBlockRange().length-=1
241 self.currentBlockRange().length-=1
241 return False
242 return False
242 return False
243 return False
243
244
244
245
245 def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings):
246 def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings):
246 """
247 """
247 Delegate method for NSTextView.
248 Delegate method for NSTextView.
248
249
249 Refuse change text in ranges not at end, but make those changes at end.
250 Refuse change text in ranges not at end, but make those changes at end.
250 """
251 """
251
252
252 #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings
253 #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings
253 assert(len(ranges) == len(replacementStrings))
254 assert(len(ranges) == len(replacementStrings))
254 allow = True
255 allow = True
255 for r,s in zip(ranges, replacementStrings):
256 for r,s in zip(ranges, replacementStrings):
256 r = r.rangeValue()
257 r = r.rangeValue()
257 if(textView.textStorage().length() > 0 and
258 if(textView.textStorage().length() > 0 and
258 r.location < self.currentBlockRange().location):
259 r.location < self.currentBlockRange().location):
259 self.insert_text(s)
260 self.insert_text(s)
260 allow = False
261 allow = False
261
262
262
263
263 self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s)
264 self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s)
264
265
265 return allow
266 return allow
266
267
267 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index):
268 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index):
268 try:
269 try:
269 token = textView.textStorage().string().substringWithRange_(charRange)
270 token = textView.textStorage().string().substringWithRange_(charRange)
270 completions = blockingCallFromThread(self.complete, token)
271 completions = blockingCallFromThread(self.complete, token)
271 except:
272 except:
272 completions = objc.nil
273 completions = objc.nil
273 NSBeep()
274 NSBeep()
274
275
275 return (completions,0)
276 return (completions,0)
276
277
277
278
278 def startNewBlock(self):
279 def startNewBlock(self):
279 """"""
280 """"""
280
281
281 self.currentBlockID = self.nextBlockID()
282 self.currentBlockID = self.nextBlockID()
282
283
283
284
284
285
285 def nextBlockID(self):
286 def nextBlockID(self):
286
287
287 return uuid.uuid4()
288 return uuid.uuid4()
288
289
289 def currentBlockRange(self):
290 def currentBlockRange(self):
290 return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0))
291 return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0))
291
292
292 def currentBlock(self):
293 def currentBlock(self):
293 """The current block's text"""
294 """The current block's text"""
294
295
295 return self.textForRange(self.currentBlockRange())
296 return self.textForRange(self.currentBlockRange())
296
297
297 def textForRange(self, textRange):
298 def textForRange(self, textRange):
298 """textForRange"""
299 """textForRange"""
299
300
300 return self.textView.textStorage().string().substringWithRange_(textRange)
301 return self.textView.textStorage().string().substringWithRange_(textRange)
301
302
302 def currentLine(self):
303 def currentLine(self):
303 block = self.textForRange(self.currentBlockRange())
304 block = self.textForRange(self.currentBlockRange())
304 block = block.split('\n')
305 block = block.split('\n')
305 return block[-1]
306 return block[-1]
306
307
307 def update_cell_prompt(self, result):
308 def update_cell_prompt(self, result):
308 blockID = result['blockID']
309 if(isinstance(result, Failure)):
310 blockID = result.blockID
311 else:
312 blockID = result['blockID']
313
314
309 self.insert_text(self.input_prompt(result=result),
315 self.insert_text(self.input_prompt(result=result),
310 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
316 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
311 scrollToVisible=False
317 scrollToVisible=False
312 )
318 )
313
319
314 return result
320 return result
315
321
316
322
317 def render_result(self, result):
323 def render_result(self, result):
318 blockID = result['blockID']
324 blockID = result['blockID']
319 inputRange = self.blockRanges[blockID]
325 inputRange = self.blockRanges[blockID]
320 del self.blockRanges[blockID]
326 del self.blockRanges[blockID]
321
327
322 #print inputRange,self.currentBlockRange()
328 #print inputRange,self.currentBlockRange()
323 self.insert_text('\n' +
329 self.insert_text('\n' +
324 self.output_prompt(result) +
330 self.output_prompt(result) +
325 result.get('display',{}).get('pprint','') +
331 result.get('display',{}).get('pprint','') +
326 '\n\n',
332 '\n\n',
327 textRange=NSMakeRange(inputRange.location+inputRange.length, 0))
333 textRange=NSMakeRange(inputRange.location+inputRange.length, 0))
328 return result
334 return result
329
335
330
336
331 def render_error(self, failure):
337 def render_error(self, failure):
332 self.insert_text('\n\n'+str(failure)+'\n\n')
338 self.insert_text('\n\n'+str(failure)+'\n\n')
333 self.startNewBlock()
339 self.startNewBlock()
334 return failure
340 return failure
335
341
336
342
337 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
343 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
338 """Insert text into textView at textRange, updating blockRanges as necessary"""
344 """Insert text into textView at textRange, updating blockRanges as necessary"""
339
345
340 if(textRange == None):
346 if(textRange == None):
341 textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text
347 textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text
342
348
343 for r in self.blockRanges.itervalues():
349 for r in self.blockRanges.itervalues():
344 intersection = NSIntersectionRange(r,textRange)
350 intersection = NSIntersectionRange(r,textRange)
345 if(intersection.length == 0): #ranges don't intersect
351 if(intersection.length == 0): #ranges don't intersect
346 if r.location >= textRange.location:
352 if r.location >= textRange.location:
347 r.location += len(string)
353 r.location += len(string)
348 else: #ranges intersect
354 else: #ranges intersect
349 if(r.location <= textRange.location):
355 if(r.location <= textRange.location):
350 assert(intersection.length == textRange.length)
356 assert(intersection.length == textRange.length)
351 r.length += textRange.length
357 r.length += textRange.length
352 else:
358 else:
353 r.location += intersection.length
359 r.location += intersection.length
354
360
355 self.textView.replaceCharactersInRange_withString_(textRange, string) #textStorage().string()
361 self.textView.replaceCharactersInRange_withString_(textRange, string) #textStorage().string()
356 self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0))
362 self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0))
357 if(scrollToVisible):
363 if(scrollToVisible):
358 self.textView.scrollRangeToVisible_(textRange)
364 self.textView.scrollRangeToVisible_(textRange)
359
365
360
366
361
367
362 def replaceCurrentBlockWithString(self, textView, string):
368 def replaceCurrentBlockWithString(self, textView, string):
363 textView.replaceCharactersInRange_withString_(self.currentBlockRange(),
369 textView.replaceCharactersInRange_withString_(self.currentBlockRange(),
364 string)
370 string)
365 self.currentBlockRange().length = len(string)
371 self.currentBlockRange().length = len(string)
366 r = NSMakeRange(textView.textStorage().length(), 0)
372 r = NSMakeRange(textView.textStorage().length(), 0)
367 textView.scrollRangeToVisible_(r)
373 textView.scrollRangeToVisible_(r)
368 textView.setSelectedRange_(r)
374 textView.setSelectedRange_(r)
369
375
370
376
371 def currentIndentString(self):
377 def currentIndentString(self):
372 """returns string for indent or None if no indent"""
378 """returns string for indent or None if no indent"""
373
379
374 if(len(self.currentBlock()) > 0):
380 if(len(self.currentBlock()) > 0):
375 lines = self.currentBlock().split('\n')
381 lines = self.currentBlock().split('\n')
376 currentIndent = len(lines[-1]) - len(lines[-1])
382 currentIndent = len(lines[-1]) - len(lines[-1])
377 if(currentIndent == 0):
383 if(currentIndent == 0):
378 currentIndent = self.tabSpaces
384 currentIndent = self.tabSpaces
379
385
380 if(self.tabUsesSpaces):
386 if(self.tabUsesSpaces):
381 result = ' ' * currentIndent
387 result = ' ' * currentIndent
382 else:
388 else:
383 result = '\t' * (currentIndent/self.tabSpaces)
389 result = '\t' * (currentIndent/self.tabSpaces)
384 else:
390 else:
385 result = None
391 result = None
386
392
387 return result
393 return result
388
394
389
395
@@ -1,311 +1,326 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 frontendbase provides an interface and base class for GUI frontends for IPython.kernel/IPython.kernel.core.
3 frontendbase provides an interface and base class for GUI frontends for IPython.kernel/IPython.kernel.core.
4
4
5 Frontend implementations will likely want to subclass FrontEndBase.
5 Frontend implementations will likely want to subclass FrontEndBase.
6
6
7 Author: Barry Wark
7 Author: Barry Wark
8 """
8 """
9 __docformat__ = "restructuredtext en"
9 __docformat__ = "restructuredtext en"
10
10
11 #-------------------------------------------------------------------------------
11 #-------------------------------------------------------------------------------
12 # Copyright (C) 2008 The IPython Development Team
12 # Copyright (C) 2008 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #-------------------------------------------------------------------------------
16 #-------------------------------------------------------------------------------
17
17
18 #-------------------------------------------------------------------------------
18 #-------------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21 import string
21 import string
22 import uuid
22 import uuid
23 import _ast
23 import _ast
24
24
25 import zope.interface as zi
25 import zope.interface as zi
26
26
27 from IPython.kernel.core.history import FrontEndHistory
27 from IPython.kernel.core.history import FrontEndHistory
28 from IPython.kernel.core.util import Bunch
28 from IPython.kernel.core.util import Bunch
29 from IPython.kernel.engineservice import IEngineCore
29 from IPython.kernel.engineservice import IEngineCore
30
30
31 from twisted.python.failure import Failure
31 from twisted.python.failure import Failure
32
32
33 ##############################################################################
33 ##############################################################################
34 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
34 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
35 # not
35 # not
36
36
37 rc = Bunch()
37 rc = Bunch()
38 rc.prompt_in1 = r'In [$number]: '
38 rc.prompt_in1 = r'In [$number]: '
39 rc.prompt_in2 = r'...'
39 rc.prompt_in2 = r'...'
40 rc.prompt_out = r'Out [$number]: '
40 rc.prompt_out = r'Out [$number]: '
41
41
42 ##############################################################################
42 ##############################################################################
43
43
44 class IFrontEndFactory(zi.Interface):
44 class IFrontEndFactory(zi.Interface):
45 """Factory interface for frontends."""
45 """Factory interface for frontends."""
46
46
47 def __call__(engine=None, history=None):
47 def __call__(engine=None, history=None):
48 """
48 """
49 Parameters:
49 Parameters:
50 interpreter : IPython.kernel.engineservice.IEngineCore
50 interpreter : IPython.kernel.engineservice.IEngineCore
51 """
51 """
52
52
53 pass
53 pass
54
54
55
55
56
56
57 class IFrontEnd(zi.Interface):
57 class IFrontEnd(zi.Interface):
58 """Interface for frontends. All methods return t.i.d.Deferred"""
58 """Interface for frontends. All methods return t.i.d.Deferred"""
59
59
60 zi.Attribute("input_prompt_template", "string.Template instance substituteable with execute result.")
60 zi.Attribute("input_prompt_template", "string.Template instance substituteable with execute result.")
61 zi.Attribute("output_prompt_template", "string.Template instance substituteable with execute result.")
61 zi.Attribute("output_prompt_template", "string.Template instance substituteable with execute result.")
62 zi.Attribute("continuation_prompt_template", "string.Template instance substituteable with execute result.")
62 zi.Attribute("continuation_prompt_template", "string.Template instance substituteable with execute result.")
63
63
64 def update_cell_prompt(self, result):
64 def update_cell_prompt(self, result):
65 """Subclass may override to update the input prompt for a block.
65 """Subclass may override to update the input prompt for a block.
66 Since this method will be called as a twisted.internet.defer.Deferred's callback,
66 Since this method will be called as a twisted.internet.defer.Deferred's callback,
67 implementations should return result when finished."""
67 implementations should return result when finished."""
68
68
69 pass
69 pass
70
70
71 def render_result(self, result):
71 def render_result(self, result):
72 """Render the result of an execute call. Implementors may choose the method of rendering.
72 """Render the result of an execute call. Implementors may choose the method of rendering.
73 For example, a notebook-style frontend might render a Chaco plot inline.
73 For example, a notebook-style frontend might render a Chaco plot inline.
74
74
75 Parameters:
75 Parameters:
76 result : dict (result of IEngineBase.execute )
76 result : dict (result of IEngineBase.execute )
77
77
78 Result:
78 Result:
79 Output of frontend rendering
79 Output of frontend rendering
80 """
80 """
81
81
82 pass
82 pass
83
83
84 def render_error(self, failure):
84 def render_error(self, failure):
85 """Subclasses must override to render the failure. Since this method will be called as a
85 """Subclasses must override to render the failure. Since this method will be called as a
86 twisted.internet.defer.Deferred's callback, implementations should return result
86 twisted.internet.defer.Deferred's callback, implementations should return result
87 when finished."""
87 when finished."""
88
88
89 pass
89 pass
90
90
91
91
92 def input_prompt(result={}):
92 def input_prompt(result={}):
93 """Returns the input prompt by subsituting into self.input_prompt_template"""
93 """Returns the input prompt by subsituting into self.input_prompt_template"""
94 pass
94 pass
95
95
96 def output_prompt(result):
96 def output_prompt(result):
97 """Returns the output prompt by subsituting into self.output_prompt_template"""
97 """Returns the output prompt by subsituting into self.output_prompt_template"""
98
98
99 pass
99 pass
100
100
101 def continuation_prompt():
101 def continuation_prompt():
102 """Returns the continuation prompt by subsituting into self.continuation_prompt_template"""
102 """Returns the continuation prompt by subsituting into self.continuation_prompt_template"""
103
103
104 pass
104 pass
105
105
106 def is_complete(block):
106 def is_complete(block):
107 """Returns True if block is complete, False otherwise."""
107 """Returns True if block is complete, False otherwise."""
108
108
109 pass
109 pass
110
110
111 def compile_ast(block):
111 def compile_ast(block):
112 """Compiles block to an _ast.AST"""
112 """Compiles block to an _ast.AST"""
113
113
114 pass
114 pass
115
115
116
116
117 def get_history_previous(currentBlock):
117 def get_history_previous(currentBlock):
118 """Returns the block previous in the history. Saves currentBlock if
118 """Returns the block previous in the history. Saves currentBlock if
119 the history_cursor is currently at the end of the input history"""
119 the history_cursor is currently at the end of the input history"""
120 pass
120 pass
121
121
122 def get_history_next():
122 def get_history_next():
123 """Returns the next block in the history."""
123 """Returns the next block in the history."""
124
124
125 pass
125 pass
126
126
127
127
128 class FrontEndBase(object):
128 class FrontEndBase(object):
129 """
129 """
130 FrontEndBase manages the state tasks for a CLI frontend:
130 FrontEndBase manages the state tasks for a CLI frontend:
131 - Input and output history management
131 - Input and output history management
132 - Input/continuation and output prompt generation
132 - Input/continuation and output prompt generation
133
133
134 Some issues (due to possibly unavailable engine):
134 Some issues (due to possibly unavailable engine):
135 - How do we get the current cell number for the engine?
135 - How do we get the current cell number for the engine?
136 - How do we handle completions?
136 - How do we handle completions?
137 """
137 """
138
138
139 zi.implements(IFrontEnd)
139 zi.implements(IFrontEnd)
140 zi.classProvides(IFrontEndFactory)
140 zi.classProvides(IFrontEndFactory)
141
141
142 history_cursor = 0
142 history_cursor = 0
143
143
144 current_indent_level = 0
144 current_indent_level = 0
145
145
146
146
147 input_prompt_template = string.Template(rc.prompt_in1)
147 input_prompt_template = string.Template(rc.prompt_in1)
148 output_prompt_template = string.Template(rc.prompt_out)
148 output_prompt_template = string.Template(rc.prompt_out)
149 continuation_prompt_template = string.Template(rc.prompt_in2)
149 continuation_prompt_template = string.Template(rc.prompt_in2)
150
150
151 def __init__(self, engine=None, history=None):
151 def __init__(self, engine=None, history=None):
152 assert(engine==None or IEngineCore.providedBy(engine))
152 assert(engine==None or IEngineCore.providedBy(engine))
153 self.engine = IEngineCore(engine)
153 self.engine = IEngineCore(engine)
154 if history is None:
154 if history is None:
155 self.history = FrontEndHistory(input_cache=[''])
155 self.history = FrontEndHistory(input_cache=[''])
156 else:
156 else:
157 self.history = history
157 self.history = history
158
158
159
159
160 def input_prompt(self, result={}):
160 def input_prompt(self, result={}):
161 """Returns the current input prompt
161 """Returns the current input prompt
162
162
163 It would be great to use ipython1.core.prompts.Prompt1 here
163 It would be great to use ipython1.core.prompts.Prompt1 here
164 """
164 """
165
165
166 result.setdefault('number','')
166 result.setdefault('number','')
167
167
168 return self.input_prompt_template.safe_substitute(result)
168 return self.input_prompt_template.safe_substitute(result)
169
169
170
170
171 def continuation_prompt(self):
171 def continuation_prompt(self):
172 """Returns the current continuation prompt"""
172 """Returns the current continuation prompt"""
173
173
174 return self.continuation_prompt_template.safe_substitute()
174 return self.continuation_prompt_template.safe_substitute()
175
175
176 def output_prompt(self, result):
176 def output_prompt(self, result):
177 """Returns the output prompt for result"""
177 """Returns the output prompt for result"""
178
178
179 return self.output_prompt_template.safe_substitute(result)
179 return self.output_prompt_template.safe_substitute(result)
180
180
181
181
182 def is_complete(self, block):
182 def is_complete(self, block):
183 """Determine if block is complete.
183 """Determine if block is complete.
184
184
185 Parameters
185 Parameters
186 block : string
186 block : string
187
187
188 Result
188 Result
189 True if block can be sent to the engine without compile errors.
189 True if block can be sent to the engine without compile errors.
190 False otherwise.
190 False otherwise.
191 """
191 """
192
192
193 try:
193 try:
194 ast = self.compile_ast(block)
194 ast = self.compile_ast(block)
195 except:
195 except:
196 return False
196 return False
197
197
198 lines = block.split('\n')
198 lines = block.split('\n')
199 return (len(lines)==1 or str(lines[-1])=='')
199 return (len(lines)==1 or str(lines[-1])=='')
200
200
201
201
202 def compile_ast(self, block):
202 def compile_ast(self, block):
203 """Compile block to an AST
203 """Compile block to an AST
204
204
205 Parameters:
205 Parameters:
206 block : str
206 block : str
207
207
208 Result:
208 Result:
209 AST
209 AST
210
210
211 Throws:
211 Throws:
212 Exception if block cannot be compiled
212 Exception if block cannot be compiled
213 """
213 """
214
214
215 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
215 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
216
216
217
217
218 def execute(self, block, blockID=None):
218 def execute(self, block, blockID=None):
219 """Execute the block and return result.
219 """Execute the block and return result.
220
220
221 Parameters:
221 Parameters:
222 block : {str, AST}
222 block : {str, AST}
223 blockID : any
223 blockID : any
224 Caller may provide an ID to identify this block. result['blockID'] := blockID
224 Caller may provide an ID to identify this block. result['blockID'] := blockID
225
225
226 Result:
226 Result:
227 Deferred result of self.interpreter.execute
227 Deferred result of self.interpreter.execute
228 """
228 """
229
229
230 if(not self.is_complete(block)):
230 if(not self.is_complete(block)):
231 return Failure(Exception("Block is not compilable"))
231 return Failure(Exception("Block is not compilable"))
232
232
233 if(blockID == None):
233 if(blockID == None):
234 blockID = uuid.uuid4() #random UUID
234 blockID = uuid.uuid4() #random UUID
235
235
236 d = self.engine.execute(block)
236 d = self.engine.execute(block)
237 d.addCallback(self._add_block_id, blockID)
238 d.addCallback(self._add_history, block=block)
237 d.addCallback(self._add_history, block=block)
239 d.addCallback(self.update_cell_prompt)
238 d.addBoth(self._add_block_id, blockID)
239 d.addBoth(self.update_cell_prompt)
240 d.addCallbacks(self.render_result, errback=self.render_error)
240 d.addCallbacks(self.render_result, errback=self.render_error)
241
241
242 return d
242 return d
243
243
244
244
245 def _add_block_id(self, result, blockID):
245 def _add_block_id(self, result, blockID):
246 """Add the blockID to result"""
246 """Add the blockID to result or failure. Unfortunatley, we have to treat failures
247 differently than result dicts
248 """
247
249
248 result['blockID'] = blockID
250 if(isinstance(result, Failure)):
251 result.blockID = blockID
252 else:
253 result['blockID'] = blockID
249
254
250 return result
255 return result
251
256
252 def _add_history(self, result, block=None):
257 def _add_history(self, result, block=None):
253 """Add block to the history"""
258 """Add block to the history"""
254
259
255 assert(block != None)
260 assert(block != None)
256 self.history.add_items([block])
261 self.history.add_items([block])
257 self.history_cursor += 1
262 self.history_cursor += 1
258
263
259 return result
264 return result
260
265
261
266
262 def get_history_previous(self, currentBlock):
267 def get_history_previous(self, currentBlock):
263 """ Returns previous history string and decrement history cursor.
268 """ Returns previous history string and decrement history cursor.
264 """
269 """
265 command = self.history.get_history_item(self.history_cursor - 1)
270 command = self.history.get_history_item(self.history_cursor - 1)
266
271
267 if command is not None:
272 if command is not None:
268 if(self.history_cursor == len(self.history.input_cache)):
273 if(self.history_cursor == len(self.history.input_cache)):
269 self.history.input_cache[self.history_cursor] = currentBlock
274 self.history.input_cache[self.history_cursor] = currentBlock
270 self.history_cursor -= 1
275 self.history_cursor -= 1
271 return command
276 return command
272
277
273
278
274 def get_history_next(self):
279 def get_history_next(self):
275 """ Returns next history string and increment history cursor.
280 """ Returns next history string and increment history cursor.
276 """
281 """
277 command = self.history.get_history_item(self.history_cursor+1)
282 command = self.history.get_history_item(self.history_cursor+1)
278
283
279 if command is not None:
284 if command is not None:
280 self.history_cursor += 1
285 self.history_cursor += 1
281 return command
286 return command
282
287
283 ###
288 ###
284 # Subclasses probably want to override these methods...
289 # Subclasses probably want to override these methods...
285 ###
290 ###
286
291
287 def update_cell_prompt(self, result):
292 def update_cell_prompt(self, result):
288 """Subclass may override to update the input prompt for a block.
293 """Subclass may override to update the input prompt for a block.
289 Since this method will be called as a twisted.internet.defer.Deferred's callback,
294 Since this method will be called as a twisted.internet.defer.Deferred's callback,
290 implementations should return result when finished."""
295 implementations should return result when finished.
296
297 NP: result is a failure if the execute returned a failre. To get the blockID, you should
298 do something like::
299 if(isinstance(result, twisted.python.failure.Failure)):
300 blockID = result.blockID
301 else:
302 blockID = result['blockID']
303
304
305 """
291
306
292 return result
307 return result
293
308
294
309
295 def render_result(self, result):
310 def render_result(self, result):
296 """Subclasses must override to render result. Since this method will be called as a
311 """Subclasses must override to render result. Since this method will be called as a
297 twisted.internet.defer.Deferred's callback, implementations should return result
312 twisted.internet.defer.Deferred's callback, implementations should return result
298 when finished."""
313 when finished."""
299
314
300 return result
315 return result
301
316
302
317
303 def render_error(self, failure):
318 def render_error(self, failure):
304 """Subclasses must override to render the failure. Since this method will be called as a
319 """Subclasses must override to render the failure. Since this method will be called as a
305 twisted.internet.defer.Deferred's callback, implementations should return result
320 twisted.internet.defer.Deferred's callback, implementations should return result
306 when finished."""
321 when finished."""
307
322
308 return failure
323 return failure
309
324
310
325
311
326
@@ -1,139 +1,149 b''
1 # encoding: utf-8
1 # encoding: utf-8
2
2
3 """This file contains unittests for the frontendbase module."""
3 """This file contains unittests for the frontendbase module."""
4
4
5 __docformat__ = "restructuredtext en"
5 __docformat__ = "restructuredtext en"
6
6
7 #-------------------------------------------------------------------------------
7 #-------------------------------------------------------------------------------
8 # Copyright (C) 2008 The IPython Development Team
8 # Copyright (C) 2008 The IPython Development Team
9 #
9 #
10 # Distributed under the terms of the BSD License. The full license is in
10 # Distributed under the terms of the BSD License. The full license is in
11 # the file COPYING, distributed as part of this software.
11 # the file COPYING, distributed as part of this software.
12 #-------------------------------------------------------------------------------
12 #-------------------------------------------------------------------------------
13
13
14 #-------------------------------------------------------------------------------
14 #-------------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-------------------------------------------------------------------------------
16 #-------------------------------------------------------------------------------
17
17
18 import unittest
18 import unittest
19 from IPython.frontend import frontendbase
19 from IPython.frontend import frontendbase
20 from IPython.kernel.engineservice import EngineService
20 from IPython.kernel.engineservice import EngineService
21
21
22 class FrontEndCallbackChecker(frontendbase.FrontEndBase):
22 class FrontEndCallbackChecker(frontendbase.FrontEndBase):
23 """FrontEndBase subclass for checking callbacks"""
23 """FrontEndBase subclass for checking callbacks"""
24 def __init__(self, engine=None, history=None):
24 def __init__(self, engine=None, history=None):
25 super(FrontEndCallbackChecker, self).__init__(engine=engine, history=history)
25 super(FrontEndCallbackChecker, self).__init__(engine=engine, history=history)
26 self.updateCalled = False
26 self.updateCalled = False
27 self.renderResultCalled = False
27 self.renderResultCalled = False
28 self.renderErrorCalled = False
28 self.renderErrorCalled = False
29
29
30 def update_cell_prompt(self, result):
30 def update_cell_prompt(self, result):
31 self.updateCalled = True
31 self.updateCalled = True
32 return result
32 return result
33
33
34 def render_result(self, result):
34 def render_result(self, result):
35 self.renderResultCalled = True
35 self.renderResultCalled = True
36 return result
36 return result
37
37
38
38
39 def render_error(self, failure):
39 def render_error(self, failure):
40 self.renderErrorCalled = True
40 self.renderErrorCalled = True
41 return failure
41 return failure
42
42
43
43
44
44
45
45
46 class TestFrontendBase(unittest.TestCase):
46 class TestFrontendBase(unittest.TestCase):
47 def setUp(self):
47 def setUp(self):
48 """Setup the EngineService and FrontEndBase"""
48 """Setup the EngineService and FrontEndBase"""
49
49
50 self.fb = FrontEndCallbackChecker(engine=EngineService())
50 self.fb = FrontEndCallbackChecker(engine=EngineService())
51
51
52
52
53 def test_implements_IFrontEnd(self):
53 def test_implements_IFrontEnd(self):
54 assert(frontendbase.IFrontEnd.implementedBy(frontendbase.FrontEndBase))
54 assert(frontendbase.IFrontEnd.implementedBy(frontendbase.FrontEndBase))
55
55
56
56
57 def test_is_complete_returns_False_for_incomplete_block(self):
57 def test_is_complete_returns_False_for_incomplete_block(self):
58 """"""
58 """"""
59
59
60 block = """def test(a):"""
60 block = """def test(a):"""
61
61
62 assert(self.fb.is_complete(block) == False)
62 assert(self.fb.is_complete(block) == False)
63
63
64 def test_is_complete_returns_True_for_complete_block(self):
64 def test_is_complete_returns_True_for_complete_block(self):
65 """"""
65 """"""
66
66
67 block = """def test(a): pass"""
67 block = """def test(a): pass"""
68
68
69 assert(self.fb.is_complete(block))
69 assert(self.fb.is_complete(block))
70
70
71 block = """a=3"""
71 block = """a=3"""
72
72
73 assert(self.fb.is_complete(block))
73 assert(self.fb.is_complete(block))
74
74
75
75
76 def test_blockID_added_to_result(self):
76 def test_blockID_added_to_result(self):
77 block = """3+3"""
77 block = """3+3"""
78
78
79 d = self.fb.execute(block, blockID='TEST_ID')
79 d = self.fb.execute(block, blockID='TEST_ID')
80
80
81 d.addCallback(self.checkBlockID, expected='TEST_ID')
81 d.addCallback(self.checkBlockID, expected='TEST_ID')
82
82
83 def test_blockID_added_to_failure(self):
84 block = "raise Exception()"
85
86 d = self.fb.execute(block,blockID='TEST_ID')
87 d.addErrback(self.checkFailureID, expected='TEST_ID')
88
83 def checkBlockID(self, result, expected=""):
89 def checkBlockID(self, result, expected=""):
84 assert(result['blockID'] == expected)
90 assert(result['blockID'] == expected)
85
91
86
92
93 def checkFailureID(self, failure, expected=""):
94 assert(failure.blockID == expected)
95
96
87 def test_callbacks_added_to_execute(self):
97 def test_callbacks_added_to_execute(self):
88 """test that
98 """test that
89 update_cell_prompt
99 update_cell_prompt
90 render_result
100 render_result
91
101
92 are added to execute request
102 are added to execute request
93 """
103 """
94
104
95 d = self.fb.execute("10+10")
105 d = self.fb.execute("10+10")
96 d.addCallback(self.checkCallbacks)
106 d.addCallback(self.checkCallbacks)
97
107
98
108
99 def checkCallbacks(self, result):
109 def checkCallbacks(self, result):
100 assert(self.fb.updateCalled)
110 assert(self.fb.updateCalled)
101 assert(self.fb.renderResultCalled)
111 assert(self.fb.renderResultCalled)
102
112
103
113
104 def test_error_callback_added_to_execute(self):
114 def test_error_callback_added_to_execute(self):
105 """test that render_error called on execution error"""
115 """test that render_error called on execution error"""
106
116
107 d = self.fb.execute("raise Exception()")
117 d = self.fb.execute("raise Exception()")
108 d.addCallback(self.checkRenderError)
118 d.addCallback(self.checkRenderError)
109
119
110 def checkRenderError(self, result):
120 def checkRenderError(self, result):
111 assert(self.fb.renderErrorCalled)
121 assert(self.fb.renderErrorCalled)
112
122
113 def test_history_returns_expected_block(self):
123 def test_history_returns_expected_block(self):
114 """Make sure history browsing doesn't fail"""
124 """Make sure history browsing doesn't fail"""
115
125
116 blocks = ["a=1","a=2","a=3"]
126 blocks = ["a=1","a=2","a=3"]
117 for b in blocks:
127 for b in blocks:
118 d = self.fb.execute(b)
128 d = self.fb.execute(b)
119
129
120 # d is now the deferred for the last executed block
130 # d is now the deferred for the last executed block
121 d.addCallback(self.historyTests, blocks)
131 d.addCallback(self.historyTests, blocks)
122
132
123
133
124 def historyTests(self, result, blocks):
134 def historyTests(self, result, blocks):
125 """historyTests"""
135 """historyTests"""
126
136
127 assert(len(blocks) >= 3)
137 assert(len(blocks) >= 3)
128 assert(self.fb.get_history_previous("") == blocks[-2])
138 assert(self.fb.get_history_previous("") == blocks[-2])
129 assert(self.fb.get_history_previous("") == blocks[-3])
139 assert(self.fb.get_history_previous("") == blocks[-3])
130 assert(self.fb.get_history_next() == blocks[-2])
140 assert(self.fb.get_history_next() == blocks[-2])
131
141
132
142
133 def test_history_returns_none_at_startup(self):
143 def test_history_returns_none_at_startup(self):
134 """test_history_returns_none_at_startup"""
144 """test_history_returns_none_at_startup"""
135
145
136 assert(self.fb.get_history_previous("")==None)
146 assert(self.fb.get_history_previous("")==None)
137 assert(self.fb.get_history_next()==None)
147 assert(self.fb.get_history_next()==None)
138
148
139
149
General Comments 0
You need to be logged in to leave comments. Login now