##// END OF EJS Templates
fixes and tests for history
Barry Wark -
Show More
@@ -1,389 +1,389 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
43
44 #-------------------------------------------------------------------------------
44 #-------------------------------------------------------------------------------
45 # Classes to implement the Cocoa frontend
45 # Classes to implement the Cocoa frontend
46 #-------------------------------------------------------------------------------
46 #-------------------------------------------------------------------------------
47
47
48 # TODO:
48 # TODO:
49 # 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService?
49 # 1. use MultiEngineClient and out-of-process engine rather than ThreadedEngineService?
50 # 2. integrate Xgrid launching of engines
50 # 2. integrate Xgrid launching of engines
51
51
52
52
53
53
54
54
55 class IPythonCocoaController(NSObject, FrontEndBase):
55 class IPythonCocoaController(NSObject, FrontEndBase):
56 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
56 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
57 waitingForEngine = objc.ivar().bool()
57 waitingForEngine = objc.ivar().bool()
58 textView = objc.IBOutlet()
58 textView = objc.IBOutlet()
59
59
60 def init(self):
60 def init(self):
61 self = super(IPythonCocoaController, self).init()
61 self = super(IPythonCocoaController, self).init()
62 FrontEndBase.__init__(self, engine=ThreadedEngineService())
62 FrontEndBase.__init__(self, engine=ThreadedEngineService())
63 if(self != None):
63 if(self != None):
64 self._common_init()
64 self._common_init()
65
65
66 return self
66 return self
67
67
68 def _common_init(self):
68 def _common_init(self):
69 """_common_init"""
69 """_common_init"""
70
70
71 self.userNS = NSMutableDictionary.dictionary()
71 self.userNS = NSMutableDictionary.dictionary()
72 self.waitingForEngine = False
72 self.waitingForEngine = False
73
73
74 self.lines = {}
74 self.lines = {}
75 self.tabSpaces = 4
75 self.tabSpaces = 4
76 self.tabUsesSpaces = True
76 self.tabUsesSpaces = True
77 self.currentBlockID = self.nextBlockID()
77 self.currentBlockID = self.nextBlockID()
78 self.blockRanges = {} # blockID=>NSRange
78 self.blockRanges = {} # blockID=>NSRange
79
79
80
80
81 def awakeFromNib(self):
81 def awakeFromNib(self):
82 """awakeFromNib"""
82 """awakeFromNib"""
83
83
84 self._common_init()
84 self._common_init()
85
85
86 # Start the IPython engine
86 # Start the IPython engine
87 self.engine.startService()
87 self.engine.startService()
88 NSLog('IPython engine started')
88 NSLog('IPython engine started')
89
89
90 # Register for app termination
90 # Register for app termination
91 NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self,
91 NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self,
92 'appWillTerminate:',
92 'appWillTerminate:',
93 NSApplicationWillTerminateNotification,
93 NSApplicationWillTerminateNotification,
94 None)
94 None)
95
95
96 self.textView.setDelegate_(self)
96 self.textView.setDelegate_(self)
97 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
97 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
98 self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_(
98 self.verticalRulerView = NSRulerView.alloc().initWithScrollView_orientation_(
99 self.textView.enclosingScrollView(),
99 self.textView.enclosingScrollView(),
100 NSVerticalRuler)
100 NSVerticalRuler)
101 self.verticalRulerView.setClientView_(self.textView)
101 self.verticalRulerView.setClientView_(self.textView)
102 self.startCLIForTextView()
102 self.startCLIForTextView()
103
103
104
104
105 def appWillTerminate_(self, notification):
105 def appWillTerminate_(self, notification):
106 """appWillTerminate"""
106 """appWillTerminate"""
107
107
108 self.engine.stopService()
108 self.engine.stopService()
109
109
110
110
111 def complete(self, token):
111 def complete(self, token):
112 """Complete token in engine's user_ns
112 """Complete token in engine's user_ns
113
113
114 Parameters
114 Parameters
115 ----------
115 ----------
116 token : string
116 token : string
117
117
118 Result
118 Result
119 ------
119 ------
120 Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete
120 Deferred result of ipython1.kernel.engineservice.IEngineInteractive.complete
121 """
121 """
122
122
123 return self.engine.complete(token)
123 return self.engine.complete(token)
124
124
125
125
126 def execute(self, block, blockID=None):
126 def execute(self, block, blockID=None):
127 self.waitingForEngine = True
127 self.waitingForEngine = True
128 self.willChangeValueForKey_('commandHistory')
128 self.willChangeValueForKey_('commandHistory')
129 d = super(IPythonCocoaController, self).execute(block, blockID)
129 d = super(IPythonCocoaController, self).execute(block, blockID)
130 d.addBoth(self._engineDone)
130 d.addBoth(self._engineDone)
131 d.addCallback(self._updateUserNS)
131 d.addCallback(self._updateUserNS)
132
132
133 return d
133 return d
134
134
135
135
136 def _engineDone(self, x):
136 def _engineDone(self, x):
137 self.waitingForEngine = False
137 self.waitingForEngine = False
138 self.didChangeValueForKey_('commandHistory')
138 self.didChangeValueForKey_('commandHistory')
139 return x
139 return x
140
140
141 def _updateUserNS(self, result):
141 def _updateUserNS(self, result):
142 """Update self.userNS from self.engine's namespace"""
142 """Update self.userNS from self.engine's namespace"""
143 d = self.engine.keys()
143 d = self.engine.keys()
144 d.addCallback(self._getEngineNamepsaceValuesForKeys)
144 d.addCallback(self._getEngineNamepsaceValuesForKeys)
145
145
146 return result
146 return result
147
147
148
148
149 def _getEngineNamepsaceValuesForKeys(self, keys):
149 def _getEngineNamepsaceValuesForKeys(self, keys):
150 d = self.engine.pull(keys)
150 d = self.engine.pull(keys)
151 d.addCallback(self._storeEngineNamespaceValues, keys=keys)
151 d.addCallback(self._storeEngineNamespaceValues, keys=keys)
152
152
153
153
154 def _storeEngineNamespaceValues(self, values, keys=[]):
154 def _storeEngineNamespaceValues(self, values, keys=[]):
155 assert(len(values) == len(keys))
155 assert(len(values) == len(keys))
156 self.willChangeValueForKey_('userNS')
156 self.willChangeValueForKey_('userNS')
157 for (k,v) in zip(keys,values):
157 for (k,v) in zip(keys,values):
158 self.userNS[k] = saferepr(v)
158 self.userNS[k] = saferepr(v)
159 self.didChangeValueForKey_('userNS')
159 self.didChangeValueForKey_('userNS')
160
160
161
161
162 def startCLIForTextView(self):
162 def startCLIForTextView(self):
163 """Print banner"""
163 """Print banner"""
164
164
165 banner = """IPython1 %s -- An enhanced Interactive Python.""" % IPython.__version__
165 banner = """IPython1 %s -- An enhanced Interactive Python.""" % IPython.__version__
166
166
167 self.insert_text(banner + '\n\n')
167 self.insert_text(banner + '\n\n')
168
168
169 # NSTextView/IPythonTextView delegate methods
169 # NSTextView/IPythonTextView delegate methods
170 def textView_doCommandBySelector_(self, textView, selector):
170 def textView_doCommandBySelector_(self, textView, selector):
171 assert(textView == self.textView)
171 assert(textView == self.textView)
172 NSLog("textView_doCommandBySelector_: "+selector)
172 NSLog("textView_doCommandBySelector_: "+selector)
173
173
174
174
175 if(selector == 'insertNewline:'):
175 if(selector == 'insertNewline:'):
176 indent = self.currentIndentString()
176 indent = self.currentIndentString()
177 if(indent):
177 if(indent):
178 line = indent + self.currentLine()
178 line = indent + self.currentLine()
179 else:
179 else:
180 line = self.currentLine()
180 line = self.currentLine()
181
181
182 if(self.is_complete(self.currentBlock())):
182 if(self.is_complete(self.currentBlock())):
183 self.execute(self.currentBlock(),
183 self.execute(self.currentBlock(),
184 blockID=self.currentBlockID)
184 blockID=self.currentBlockID)
185 self.startNewBlock()
185 self.startNewBlock()
186
186
187 return True
187 return True
188
188
189 return False
189 return False
190
190
191 elif(selector == 'moveUp:'):
191 elif(selector == 'moveUp:'):
192 prevBlock = self.get_history_previous(self.currentBlock())
192 prevBlock = self.get_history_previous(self.currentBlock())
193 if(prevBlock != None):
193 if(prevBlock != None):
194 self.replaceCurrentBlockWithString(textView, prevBlock)
194 self.replaceCurrentBlockWithString(textView, prevBlock)
195 else:
195 else:
196 NSBeep()
196 NSBeep()
197 return True
197 return True
198
198
199 elif(selector == 'moveDown:'):
199 elif(selector == 'moveDown:'):
200 nextBlock = self.get_history_next(self.currentBlock())
200 nextBlock = self.get_history_next()
201 if(nextBlock != None):
201 if(nextBlock != None):
202 self.replaceCurrentBlockWithString(textView, nextBlock)
202 self.replaceCurrentBlockWithString(textView, nextBlock)
203 else:
203 else:
204 NSBeep()
204 NSBeep()
205 return True
205 return True
206
206
207 elif(selector == 'moveToBeginningOfParagraph:'):
207 elif(selector == 'moveToBeginningOfParagraph:'):
208 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0))
208 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location, 0))
209 return True
209 return True
210 elif(selector == 'moveToEndOfParagraph:'):
210 elif(selector == 'moveToEndOfParagraph:'):
211 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \
211 textView.setSelectedRange_(NSMakeRange(self.currentBlockRange().location + \
212 self.currentBlockRange().length, 0))
212 self.currentBlockRange().length, 0))
213 return True
213 return True
214 elif(selector == 'deleteToEndOfParagraph:'):
214 elif(selector == 'deleteToEndOfParagraph:'):
215 if(textView.selectedRange().location <= self.currentBlockRange().location):
215 if(textView.selectedRange().location <= self.currentBlockRange().location):
216 # Intersect the selected range with the current line range
216 # Intersect the selected range with the current line range
217 if(self.currentBlockRange().length < 0):
217 if(self.currentBlockRange().length < 0):
218 self.blockRanges[self.currentBlockID].length = 0
218 self.blockRanges[self.currentBlockID].length = 0
219
219
220 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
220 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
221 self.currentBlockRange())
221 self.currentBlockRange())
222
222
223 if(r.length > 0): #no intersection
223 if(r.length > 0): #no intersection
224 textView.setSelectedRange_(r)
224 textView.setSelectedRange_(r)
225
225
226 return False # don't actually handle the delete
226 return False # don't actually handle the delete
227
227
228 elif(selector == 'insertTab:'):
228 elif(selector == 'insertTab:'):
229 if(len(self.currentLine().strip()) == 0): #only white space
229 if(len(self.currentLine().strip()) == 0): #only white space
230 return False
230 return False
231 else:
231 else:
232 self.textView.complete_(self)
232 self.textView.complete_(self)
233 return True
233 return True
234
234
235 elif(selector == 'deleteBackward:'):
235 elif(selector == 'deleteBackward:'):
236 #if we're at the beginning of the current block, ignore
236 #if we're at the beginning of the current block, ignore
237 if(textView.selectedRange().location == self.currentBlockRange().location):
237 if(textView.selectedRange().location == self.currentBlockRange().location):
238 return True
238 return True
239 else:
239 else:
240 self.currentBlockRange().length-=1
240 self.currentBlockRange().length-=1
241 return False
241 return False
242 return False
242 return False
243
243
244
244
245 def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings):
245 def textView_shouldChangeTextInRanges_replacementStrings_(self, textView, ranges, replacementStrings):
246 """
246 """
247 Delegate method for NSTextView.
247 Delegate method for NSTextView.
248
248
249 Refuse change text in ranges not at end, but make those changes at end.
249 Refuse change text in ranges not at end, but make those changes at end.
250 """
250 """
251
251
252 #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings
252 #print 'textView_shouldChangeTextInRanges_replacementStrings_:',ranges,replacementStrings
253 assert(len(ranges) == len(replacementStrings))
253 assert(len(ranges) == len(replacementStrings))
254 allow = True
254 allow = True
255 for r,s in zip(ranges, replacementStrings):
255 for r,s in zip(ranges, replacementStrings):
256 r = r.rangeValue()
256 r = r.rangeValue()
257 if(textView.textStorage().length() > 0 and
257 if(textView.textStorage().length() > 0 and
258 r.location < self.currentBlockRange().location):
258 r.location < self.currentBlockRange().location):
259 self.insert_text(s)
259 self.insert_text(s)
260 allow = False
260 allow = False
261
261
262
262
263 self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s)
263 self.blockRanges.setdefault(self.currentBlockID, self.currentBlockRange()).length += len(s)
264
264
265 return allow
265 return allow
266
266
267 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index):
267 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self, textView, words, charRange, index):
268 try:
268 try:
269 token = textView.textStorage().string().substringWithRange_(charRange)
269 token = textView.textStorage().string().substringWithRange_(charRange)
270 completions = blockingCallFromThread(self.complete, token)
270 completions = blockingCallFromThread(self.complete, token)
271 except:
271 except:
272 completions = objc.nil
272 completions = objc.nil
273 NSBeep()
273 NSBeep()
274
274
275 return (completions,0)
275 return (completions,0)
276
276
277
277
278 def startNewBlock(self):
278 def startNewBlock(self):
279 """"""
279 """"""
280
280
281 self.currentBlockID = self.nextBlockID()
281 self.currentBlockID = self.nextBlockID()
282
282
283
283
284
284
285 def nextBlockID(self):
285 def nextBlockID(self):
286
286
287 return uuid.uuid4()
287 return uuid.uuid4()
288
288
289 def currentBlockRange(self):
289 def currentBlockRange(self):
290 return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0))
290 return self.blockRanges.get(self.currentBlockID, NSMakeRange(self.textView.textStorage().length(), 0))
291
291
292 def currentBlock(self):
292 def currentBlock(self):
293 """The current block's text"""
293 """The current block's text"""
294
294
295 return self.textForRange(self.currentBlockRange())
295 return self.textForRange(self.currentBlockRange())
296
296
297 def textForRange(self, textRange):
297 def textForRange(self, textRange):
298 """textForRange"""
298 """textForRange"""
299
299
300 return self.textView.textStorage().string().substringWithRange_(textRange)
300 return self.textView.textStorage().string().substringWithRange_(textRange)
301
301
302 def currentLine(self):
302 def currentLine(self):
303 block = self.textForRange(self.currentBlockRange())
303 block = self.textForRange(self.currentBlockRange())
304 block = block.split('\n')
304 block = block.split('\n')
305 return block[-1]
305 return block[-1]
306
306
307 def update_cell_prompt(self, result):
307 def update_cell_prompt(self, result):
308 blockID = result['blockID']
308 blockID = result['blockID']
309 self.insert_text(self.inputPrompt(result=result),
309 self.insert_text(self.inputPrompt(result=result),
310 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
310 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
311 scrollToVisible=False
311 scrollToVisible=False
312 )
312 )
313
313
314 return result
314 return result
315
315
316
316
317 def render_result(self, result):
317 def render_result(self, result):
318 blockID = result['blockID']
318 blockID = result['blockID']
319 inputRange = self.blockRanges[blockID]
319 inputRange = self.blockRanges[blockID]
320 del self.blockRanges[blockID]
320 del self.blockRanges[blockID]
321
321
322 #print inputRange,self.currentBlockRange()
322 #print inputRange,self.currentBlockRange()
323 self.insert_text('\n' +
323 self.insert_text('\n' +
324 self.outputPrompt(result) +
324 self.outputPrompt(result) +
325 result.get('display',{}).get('pprint','') +
325 result.get('display',{}).get('pprint','') +
326 '\n\n',
326 '\n\n',
327 textRange=NSMakeRange(inputRange.location+inputRange.length, 0))
327 textRange=NSMakeRange(inputRange.location+inputRange.length, 0))
328 return result
328 return result
329
329
330
330
331 def render_error(self, failure):
331 def render_error(self, failure):
332 self.insert_text('\n\n'+str(failure)+'\n\n')
332 self.insert_text('\n\n'+str(failure)+'\n\n')
333 self.startNewBlock()
333 self.startNewBlock()
334 return failure
334 return failure
335
335
336
336
337 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
337 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
338 """Insert text into textView at textRange, updating blockRanges as necessary"""
338 """Insert text into textView at textRange, updating blockRanges as necessary"""
339
339
340 if(textRange == None):
340 if(textRange == None):
341 textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text
341 textRange = NSMakeRange(self.textView.textStorage().length(), 0) #range for end of text
342
342
343 for r in self.blockRanges.itervalues():
343 for r in self.blockRanges.itervalues():
344 intersection = NSIntersectionRange(r,textRange)
344 intersection = NSIntersectionRange(r,textRange)
345 if(intersection.length == 0): #ranges don't intersect
345 if(intersection.length == 0): #ranges don't intersect
346 if r.location >= textRange.location:
346 if r.location >= textRange.location:
347 r.location += len(string)
347 r.location += len(string)
348 else: #ranges intersect
348 else: #ranges intersect
349 if(r.location <= textRange.location):
349 if(r.location <= textRange.location):
350 assert(intersection.length == textRange.length)
350 assert(intersection.length == textRange.length)
351 r.length += textRange.length
351 r.length += textRange.length
352 else:
352 else:
353 r.location += intersection.length
353 r.location += intersection.length
354
354
355 self.textView.replaceCharactersInRange_withString_(textRange, string) #textStorage().string()
355 self.textView.replaceCharactersInRange_withString_(textRange, string) #textStorage().string()
356 self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0))
356 self.textView.setSelectedRange_(NSMakeRange(textRange.location+len(string), 0))
357 if(scrollToVisible):
357 if(scrollToVisible):
358 self.textView.scrollRangeToVisible_(textRange)
358 self.textView.scrollRangeToVisible_(textRange)
359
359
360
360
361
361
362 def replaceCurrentBlockWithString(self, textView, string):
362 def replaceCurrentBlockWithString(self, textView, string):
363 textView.replaceCharactersInRange_withString_(self.currentBlockRange(),
363 textView.replaceCharactersInRange_withString_(self.currentBlockRange(),
364 string)
364 string)
365 self.currentBlockRange().length = len(string)
365 self.currentBlockRange().length = len(string)
366 r = NSMakeRange(textView.textStorage().length(), 0)
366 r = NSMakeRange(textView.textStorage().length(), 0)
367 textView.scrollRangeToVisible_(r)
367 textView.scrollRangeToVisible_(r)
368 textView.setSelectedRange_(r)
368 textView.setSelectedRange_(r)
369
369
370
370
371 def currentIndentString(self):
371 def currentIndentString(self):
372 """returns string for indent or None if no indent"""
372 """returns string for indent or None if no indent"""
373
373
374 if(len(self.currentBlock()) > 0):
374 if(len(self.currentBlock()) > 0):
375 lines = self.currentBlock().split('\n')
375 lines = self.currentBlock().split('\n')
376 currentIndent = len(lines[-1]) - len(lines[-1])
376 currentIndent = len(lines[-1]) - len(lines[-1])
377 if(currentIndent == 0):
377 if(currentIndent == 0):
378 currentIndent = self.tabSpaces
378 currentIndent = self.tabSpaces
379
379
380 if(self.tabUsesSpaces):
380 if(self.tabUsesSpaces):
381 result = ' ' * currentIndent
381 result = ' ' * currentIndent
382 else:
382 else:
383 result = '\t' * (currentIndent/self.tabSpaces)
383 result = '\t' * (currentIndent/self.tabSpaces)
384 else:
384 else:
385 result = None
385 result = None
386
386
387 return result
387 return result
388
388
389
389
@@ -1,310 +1,311 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 inputPrompt(result={}):
92 def inputPrompt(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 outputPrompt(result):
96 def outputPrompt(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 continuationPrompt():
101 def continuationPrompt():
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."""
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 pass
120 pass
120
121
121 def get_history_next(currentBlock):
122 def get_history_next():
122 """Returns the next block in the history."""
123 """Returns the next block in the history."""
123
124
124 pass
125 pass
125
126
126
127
127 class FrontEndBase(object):
128 class FrontEndBase(object):
128 """
129 """
129 FrontEndBase manages the state tasks for a CLI frontend:
130 FrontEndBase manages the state tasks for a CLI frontend:
130 - Input and output history management
131 - Input and output history management
131 - Input/continuation and output prompt generation
132 - Input/continuation and output prompt generation
132
133
133 Some issues (due to possibly unavailable engine):
134 Some issues (due to possibly unavailable engine):
134 - How do we get the current cell number for the engine?
135 - How do we get the current cell number for the engine?
135 - How do we handle completions?
136 - How do we handle completions?
136 """
137 """
137
138
138 zi.implements(IFrontEnd)
139 zi.implements(IFrontEnd)
139 zi.classProvides(IFrontEndFactory)
140 zi.classProvides(IFrontEndFactory)
140
141
141 history_cursor = 0
142 history_cursor = 0
142
143
143 current_indent_level = 0
144 current_indent_level = 0
144
145
145
146
146 input_prompt_template = string.Template(rc.prompt_in1)
147 input_prompt_template = string.Template(rc.prompt_in1)
147 output_prompt_template = string.Template(rc.prompt_out)
148 output_prompt_template = string.Template(rc.prompt_out)
148 continuation_prompt_template = string.Template(rc.prompt_in2)
149 continuation_prompt_template = string.Template(rc.prompt_in2)
149
150
150 def __init__(self, engine=None, history=None):
151 def __init__(self, engine=None, history=None):
151 assert(engine==None or IEngineCore.providedBy(engine))
152 assert(engine==None or IEngineCore.providedBy(engine))
152 self.engine = IEngineCore(engine)
153 self.engine = IEngineCore(engine)
153 if history is None:
154 if history is None:
154 self.history = FrontEndHistory(input_cache=[''])
155 self.history = FrontEndHistory(input_cache=[''])
155 else:
156 else:
156 self.history = history
157 self.history = history
157
158
158
159
159 def inputPrompt(self, result={}):
160 def inputPrompt(self, result={}):
160 """Returns the current input prompt
161 """Returns the current input prompt
161
162
162 It would be great to use ipython1.core.prompts.Prompt1 here
163 It would be great to use ipython1.core.prompts.Prompt1 here
163 """
164 """
164
165
165 result.setdefault('number','')
166 result.setdefault('number','')
166
167
167 return self.input_prompt_template.safe_substitute(result)
168 return self.input_prompt_template.safe_substitute(result)
168
169
169
170
170 def continuationPrompt(self):
171 def continuationPrompt(self):
171 """Returns the current continuation prompt"""
172 """Returns the current continuation prompt"""
172
173
173 return self.continuation_prompt_template.safe_substitute()
174 return self.continuation_prompt_template.safe_substitute()
174
175
175 def outputPrompt(self, result):
176 def outputPrompt(self, result):
176 """Returns the output prompt for result"""
177 """Returns the output prompt for result"""
177
178
178 return self.output_prompt_template.safe_substitute(result)
179 return self.output_prompt_template.safe_substitute(result)
179
180
180
181
181 def is_complete(self, block):
182 def is_complete(self, block):
182 """Determine if block is complete.
183 """Determine if block is complete.
183
184
184 Parameters
185 Parameters
185 block : string
186 block : string
186
187
187 Result
188 Result
188 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.
189 False otherwise.
190 False otherwise.
190 """
191 """
191
192
192 try:
193 try:
193 ast = self.compile_ast(block)
194 ast = self.compile_ast(block)
194 except:
195 except:
195 return False
196 return False
196
197
197 lines = block.split('\n')
198 lines = block.split('\n')
198 return (len(lines)==1 or str(lines[-1])=='')
199 return (len(lines)==1 or str(lines[-1])=='')
199
200
200
201
201 def compile_ast(self, block):
202 def compile_ast(self, block):
202 """Compile block to an AST
203 """Compile block to an AST
203
204
204 Parameters:
205 Parameters:
205 block : str
206 block : str
206
207
207 Result:
208 Result:
208 AST
209 AST
209
210
210 Throws:
211 Throws:
211 Exception if block cannot be compiled
212 Exception if block cannot be compiled
212 """
213 """
213
214
214 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
215 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
215
216
216
217
217 def execute(self, block, blockID=None):
218 def execute(self, block, blockID=None):
218 """Execute the block and return result.
219 """Execute the block and return result.
219
220
220 Parameters:
221 Parameters:
221 block : {str, AST}
222 block : {str, AST}
222 blockID : any
223 blockID : any
223 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
224
225
225 Result:
226 Result:
226 Deferred result of self.interpreter.execute
227 Deferred result of self.interpreter.execute
227 """
228 """
228
229
229 if(not self.is_complete(block)):
230 if(not self.is_complete(block)):
230 return Failure(Exception("Block is not compilable"))
231 return Failure(Exception("Block is not compilable"))
231
232
232 if(blockID == None):
233 if(blockID == None):
233 blockID = uuid.uuid4() #random UUID
234 blockID = uuid.uuid4() #random UUID
234
235
235 d = self.engine.execute(block)
236 d = self.engine.execute(block)
236 d.addCallback(self._add_block_id, blockID)
237 d.addCallback(self._add_block_id, blockID)
237 d.addCallback(self._add_history, block=block)
238 d.addCallback(self._add_history, block=block)
238 d.addCallback(self.update_cell_prompt)
239 d.addCallback(self.update_cell_prompt)
239 d.addCallbacks(self.render_result, errback=self.render_error)
240 d.addCallbacks(self.render_result, errback=self.render_error)
240
241
241 return d
242 return d
242
243
243
244
244 def _add_block_id(self, result, blockID):
245 def _add_block_id(self, result, blockID):
245 """Add the blockID to result"""
246 """Add the blockID to result"""
246
247
247 result['blockID'] = blockID
248 result['blockID'] = blockID
248
249
249 return result
250 return result
250
251
251 def _add_history(self, result, block=None):
252 def _add_history(self, result, block=None):
252 """Add block to the history"""
253 """Add block to the history"""
253
254
254 assert(block != None)
255 assert(block != None)
255 self.history.add_items([block])
256 self.history.add_items([block])
256 self.history_cursor += 1
257 self.history_cursor += 1
257
258
258 return result
259 return result
259
260
260
261
261 def get_history_previous(self, currentBlock):
262 def get_history_previous(self, currentBlock):
262 """ Returns previous history string and decrement history cursor.
263 """ Returns previous history string and decrement history cursor.
263 """
264 """
264 print self.history
265 command = self.history.get_history_item(self.history_cursor - 1)
265 command = self.history.get_history_item(self.history_cursor - 1)
266 print command
266
267 if command is not None:
267 if command is not None:
268 if(self.history_cursor == len(self.history.input_cache)):
268 self.history.input_cache[self.history_cursor] = currentBlock
269 self.history.input_cache[self.history_cursor] = currentBlock
269 self.history_cursor -= 1
270 self.history_cursor -= 1
270 return command
271 return command
271
272
272
273
273 def get_history_next(self, currentBlock):
274 def get_history_next(self):
274 """ Returns next history string and increment history cursor.
275 """ Returns next history string and increment history cursor.
275 """
276 """
276 command = self.history.get_history_item(self.history_cursor + 1)
277 command = self.history.get_history_item(self.history_cursor+1)
278
277 if command is not None:
279 if command is not None:
278 self.history.input_cache[self.history_cursor] = currentBlock
279 self.history_cursor += 1
280 self.history_cursor += 1
280 return command
281 return command
281
282
282 ###
283 ###
283 # Subclasses probably want to override these methods...
284 # Subclasses probably want to override these methods...
284 ###
285 ###
285
286
286 def update_cell_prompt(self, result):
287 def update_cell_prompt(self, result):
287 """Subclass may override to update the input prompt for a block.
288 """Subclass may override to update the input prompt for a block.
288 Since this method will be called as a twisted.internet.defer.Deferred's callback,
289 Since this method will be called as a twisted.internet.defer.Deferred's callback,
289 implementations should return result when finished."""
290 implementations should return result when finished."""
290
291
291 return result
292 return result
292
293
293
294
294 def render_result(self, result):
295 def render_result(self, result):
295 """Subclasses must override to render result. Since this method will be called as a
296 """Subclasses must override to render result. Since this method will be called as a
296 twisted.internet.defer.Deferred's callback, implementations should return result
297 twisted.internet.defer.Deferred's callback, implementations should return result
297 when finished."""
298 when finished."""
298
299
299 return result
300 return result
300
301
301
302
302 def render_error(self, failure):
303 def render_error(self, failure):
303 """Subclasses must override to render the failure. Since this method will be called as a
304 """Subclasses must override to render the failure. Since this method will be called as a
304 twisted.internet.defer.Deferred's callback, implementations should return result
305 twisted.internet.defer.Deferred's callback, implementations should return result
305 when finished."""
306 when finished."""
306
307
307 return failure
308 return failure
308
309
309
310
310
311
@@ -1,114 +1,139 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_implementsIFrontEnd(self):
53 def test_implementsIFrontEnd(self):
54 assert(frontendbase.IFrontEnd.implementedBy(frontendbase.FrontEndBase))
54 assert(frontendbase.IFrontEnd.implementedBy(frontendbase.FrontEndBase))
55
55
56
56
57 def test_is_completeReturnsFalseForIncompleteBlock(self):
57 def test_is_completeReturnsFalseForIncompleteBlock(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_completeReturnsTrueForCompleteBlock(self):
64 def test_is_completeReturnsTrueForCompleteBlock(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_blockIDAddedToResult(self):
76 def test_blockIDAddedToResult(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 checkBlockID(self, result, expected=""):
83 def checkBlockID(self, result, expected=""):
84 assert(result['blockID'] == expected)
84 assert(result['blockID'] == expected)
85
85
86
86
87 def test_callbacksAddedToExecuteRequest(self):
87 def test_callbacksAddedToExecuteRequest(self):
88 """test that
88 """test that
89 update_cell_prompt
89 update_cell_prompt
90 render_result
90 render_result
91
91
92 are added to execute request
92 are added to execute request
93 """
93 """
94
94
95 d = self.fb.execute("10+10")
95 d = self.fb.execute("10+10")
96 d.addCallback(self.checkCallbacks)
96 d.addCallback(self.checkCallbacks)
97
97
98
98
99 def checkCallbacks(self, result):
99 def checkCallbacks(self, result):
100 assert(self.fb.updateCalled)
100 assert(self.fb.updateCalled)
101 assert(self.fb.renderResultCalled)
101 assert(self.fb.renderResultCalled)
102
102
103
103
104 def test_errorCallbackAddedToExecuteRequest(self):
104 def test_errorCallbackAddedToExecuteRequest(self):
105 """test that render_error called on execution error"""
105 """test that render_error called on execution error"""
106
106
107 d = self.fb.execute("raise Exception()")
107 d = self.fb.execute("raise Exception()")
108 d.addCallback(self.checkRenderError)
108 d.addCallback(self.checkRenderError)
109
109
110 def checkRenderError(self, result):
110 def checkRenderError(self, result):
111 assert(self.fb.renderErrorCalled)
111 assert(self.fb.renderErrorCalled)
112
112
113 # TODO: add tests for history
113 def test_history_returns_expected_block(self):
114 """Make sure history browsing doesn't fail"""
115
116 blocks = ["a=1","a=2","a=3"]
117 for b in blocks:
118 d = self.fb.execute(b)
119
120 # d is now the deferred for the last executed block
121 d.addCallback(self.historyTests, blocks)
122
123
124 def historyTests(self, result, blocks):
125 """historyTests"""
126
127 assert(len(blocks) >= 3)
128 assert(self.fb.get_history_previous("") == blocks[-2])
129 assert(self.fb.get_history_previous("") == blocks[-3])
130 assert(self.fb.get_history_next() == blocks[-2])
131
132
133 def test_history_returns_none_at_startup(self):
134 """test_history_returns_none_at_startup"""
135
136 assert(self.fb.get_history_previous("")==None)
137 assert(self.fb.get_history_next()==None)
138
114
139
General Comments 0
You need to be logged in to leave comments. Login now