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