##// END OF EJS Templates
improved error rendering for cocoa_frontend
Barry Wark -
Show More
@@ -1,482 +1,504 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3
3
4 """PyObjC classes to provide a Cocoa frontend to the
4 """PyObjC classes to provide a Cocoa frontend to the
5 IPython.kernel.engineservice.IEngineBase.
5 IPython.kernel.engineservice.IEngineBase.
6
6
7 To add an IPython interpreter to a cocoa app, instantiate an
7 To add an IPython interpreter to a cocoa app, instantiate an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 NSTextView instance in your UI. That's it.
9 NSTextView instance in your UI. That's it.
10
10
11 Author: Barry Wark
11 Author: Barry Wark
12 """
12 """
13
13
14 __docformat__ = "restructuredtext en"
14 __docformat__ = "restructuredtext en"
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Copyright (C) 2008 The IPython Development Team
17 # Copyright (C) 2008 The IPython Development Team
18 #
18 #
19 # Distributed under the terms of the BSD License. The full license is in
19 # Distributed under the terms of the BSD License. The full license is in
20 # the file COPYING, distributed as part of this software.
20 # the file COPYING, distributed as part of this software.
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Imports
24 # Imports
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 import sys
27 import objc
28 import objc
28 import uuid
29 import uuid
29
30
30 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 NSLog, NSNotificationCenter, NSMakeRange,\
32 NSLog, NSNotificationCenter, NSMakeRange,\
32 NSLocalizedString, NSIntersectionRange,\
33 NSLocalizedString, NSIntersectionRange,\
33 NSString, NSAutoreleasePool
34 NSString, NSAutoreleasePool
34
35
35 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
36 NSTextView, NSRulerView, NSVerticalRuler
37 NSTextView, NSRulerView, NSVerticalRuler
37
38
38 from pprint import saferepr
39 from pprint import saferepr
39
40
40 import IPython
41 import IPython
41 from IPython.kernel.engineservice import ThreadedEngineService
42 from IPython.kernel.engineservice import ThreadedEngineService
42 from IPython.frontend.frontendbase import FrontEndBase
43 from IPython.frontend.frontendbase import FrontEndBase
43
44
44 from twisted.internet.threads import blockingCallFromThread
45 from twisted.internet.threads import blockingCallFromThread
45 from twisted.python.failure import Failure
46 from twisted.python.failure import Failure
46
47
47 #------------------------------------------------------------------------------
48 #------------------------------------------------------------------------------
48 # Classes to implement the Cocoa frontend
49 # Classes to implement the Cocoa frontend
49 #------------------------------------------------------------------------------
50 #------------------------------------------------------------------------------
50
51
51 # TODO:
52 # TODO:
52 # 1. use MultiEngineClient and out-of-process engine rather than
53 # 1. use MultiEngineClient and out-of-process engine rather than
53 # ThreadedEngineService?
54 # ThreadedEngineService?
54 # 2. integrate Xgrid launching of engines
55 # 2. integrate Xgrid launching of engines
55
56
56 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
57 """Wrap all blocks in an NSAutoreleasePool"""
58 """Wrap all blocks in an NSAutoreleasePool"""
58
59
59 def wrapped_execute(self, lines):
60 def wrapped_execute(self, msg, lines):
60 """wrapped_execute"""
61 """wrapped_execute"""
61
62 try:
62 p = NSAutoreleasePool.alloc().init()
63 p = NSAutoreleasePool.alloc().init()
63 result = self.shell.execute(lines)
64 result = self.shell.execute(lines)
64 p.drain()
65 except Exception,e:
66 # This gives the following:
67 # et=exception class
68 # ev=exception class instance
69 # tb=traceback object
70 et,ev,tb = sys.exc_info()
71 # This call adds attributes to the exception value
72 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
73 # Add another attribute
74
75 # Create a new exception with the new attributes
76 e = et(ev._ipython_traceback_text)
77 e._ipython_engine_info = msg
78
79 # Re-raise
80 raise e
81 finally:
82 p.drain()
65
83
66 return result
84 return result
67
85
68 def execute(self, lines):
86 def execute(self, lines):
69 # Only import this if we are going to use this class
87 # Only import this if we are going to use this class
70 from twisted.internet import threads
88 from twisted.internet import threads
71
89
72 msg = {'engineid':self.id,
90 msg = {'engineid':self.id,
73 'method':'execute',
91 'method':'execute',
74 'args':[lines]}
92 'args':[lines]}
75
93
76 d = threads.deferToThread(self.wrapped_execute, lines)
94 d = threads.deferToThread(self.wrapped_execute, msg, lines)
77 d.addCallback(self.addIDToResult)
95 d.addCallback(self.addIDToResult)
78 return d
96 return d
79
97
80
98
81 class IPythonCocoaController(NSObject, FrontEndBase):
99 class IPythonCocoaController(NSObject, FrontEndBase):
82 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
100 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
83 waitingForEngine = objc.ivar().bool()
101 waitingForEngine = objc.ivar().bool()
84 textView = objc.IBOutlet()
102 textView = objc.IBOutlet()
85
103
86 def init(self):
104 def init(self):
87 self = super(IPythonCocoaController, self).init()
105 self = super(IPythonCocoaController, self).init()
88 FrontEndBase.__init__(self,
106 FrontEndBase.__init__(self,
89 engine=AutoreleasePoolWrappedThreadedEngineService())
107 engine=AutoreleasePoolWrappedThreadedEngineService())
90 if(self != None):
108 if(self != None):
91 self._common_init()
109 self._common_init()
92
110
93 return self
111 return self
94
112
95 def _common_init(self):
113 def _common_init(self):
96 """_common_init"""
114 """_common_init"""
97
115
98 self.userNS = NSMutableDictionary.dictionary()
116 self.userNS = NSMutableDictionary.dictionary()
99 self.waitingForEngine = False
117 self.waitingForEngine = False
100
118
101 self.lines = {}
119 self.lines = {}
102 self.tabSpaces = 4
120 self.tabSpaces = 4
103 self.tabUsesSpaces = True
121 self.tabUsesSpaces = True
104 self.currentBlockID = self.next_block_ID()
122 self.currentBlockID = self.next_block_ID()
105 self.blockRanges = {} # blockID=>NSRange
123 self.blockRanges = {} # blockID=>NSRange
106
124
107
125
108 def awakeFromNib(self):
126 def awakeFromNib(self):
109 """awakeFromNib"""
127 """awakeFromNib"""
110
128
111 self._common_init()
129 self._common_init()
112
130
113 # Start the IPython engine
131 # Start the IPython engine
114 self.engine.startService()
132 self.engine.startService()
115 NSLog('IPython engine started')
133 NSLog('IPython engine started')
116
134
117 # Register for app termination
135 # Register for app termination
118 nc = NSNotificationCenter.defaultCenter()
136 nc = NSNotificationCenter.defaultCenter()
119 nc.addObserver_selector_name_object_(
137 nc.addObserver_selector_name_object_(
120 self,
138 self,
121 'appWillTerminate:',
139 'appWillTerminate:',
122 NSApplicationWillTerminateNotification,
140 NSApplicationWillTerminateNotification,
123 None)
141 None)
124
142
125 self.textView.setDelegate_(self)
143 self.textView.setDelegate_(self)
126 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
144 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
127 r = NSRulerView.alloc().initWithScrollView_orientation_(
145 r = NSRulerView.alloc().initWithScrollView_orientation_(
128 self.textView.enclosingScrollView(),
146 self.textView.enclosingScrollView(),
129 NSVerticalRuler)
147 NSVerticalRuler)
130 self.verticalRulerView = r
148 self.verticalRulerView = r
131 self.verticalRulerView.setClientView_(self.textView)
149 self.verticalRulerView.setClientView_(self.textView)
132 self._start_cli_banner()
150 self._start_cli_banner()
133
151
134
152
135 def appWillTerminate_(self, notification):
153 def appWillTerminate_(self, notification):
136 """appWillTerminate"""
154 """appWillTerminate"""
137
155
138 self.engine.stopService()
156 self.engine.stopService()
139
157
140
158
141 def complete(self, token):
159 def complete(self, token):
142 """Complete token in engine's user_ns
160 """Complete token in engine's user_ns
143
161
144 Parameters
162 Parameters
145 ----------
163 ----------
146 token : string
164 token : string
147
165
148 Result
166 Result
149 ------
167 ------
150 Deferred result of
168 Deferred result of
151 IPython.kernel.engineservice.IEngineBase.complete
169 IPython.kernel.engineservice.IEngineBase.complete
152 """
170 """
153
171
154 return self.engine.complete(token)
172 return self.engine.complete(token)
155
173
156
174
157 def execute(self, block, blockID=None):
175 def execute(self, block, blockID=None):
158 self.waitingForEngine = True
176 self.waitingForEngine = True
159 self.willChangeValueForKey_('commandHistory')
177 self.willChangeValueForKey_('commandHistory')
160 d = super(IPythonCocoaController, self).execute(block,
178 d = super(IPythonCocoaController, self).execute(block,
161 blockID)
179 blockID)
162 d.addBoth(self._engine_done)
180 d.addBoth(self._engine_done)
163 d.addCallback(self._update_user_ns)
181 d.addCallback(self._update_user_ns)
164
182
165 return d
183 return d
166
184
167
185
168 def push_(self, namespace):
186 def push_(self, namespace):
169 """Push dictionary of key=>values to python namespace"""
187 """Push dictionary of key=>values to python namespace"""
170
188
171 self.waitingForEngine = True
189 self.waitingForEngine = True
172 self.willChangeValueForKey_('commandHistory')
190 self.willChangeValueForKey_('commandHistory')
173 d = self.engine.push(namespace)
191 d = self.engine.push(namespace)
174 d.addBoth(self._engine_done)
192 d.addBoth(self._engine_done)
175 d.addCallback(self._update_user_ns)
193 d.addCallback(self._update_user_ns)
176
194
177
195
178 def pull_(self, keys):
196 def pull_(self, keys):
179 """Pull keys from python namespace"""
197 """Pull keys from python namespace"""
180
198
181 self.waitingForEngine = True
199 self.waitingForEngine = True
182 result = blockingCallFromThread(self.engine.pull, keys)
200 result = blockingCallFromThread(self.engine.pull, keys)
183 self.waitingForEngine = False
201 self.waitingForEngine = False
184
202
185 def executeFileAtPath_(self, path):
203 def executeFileAtPath_(self, path):
186 """Execute file at path in an empty namespace. Update the engine
204 """Execute file at path in an empty namespace. Update the engine
187 user_ns with the resulting locals."""
205 user_ns with the resulting locals."""
188
206
189 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
207 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
190 path,
208 path,
191 NSString.defaultCStringEncoding(),
209 NSString.defaultCStringEncoding(),
192 None)
210 None)
193 self.engine.execute(lines)
211 self.engine.execute(lines)
194
212
195
213
196 def _engine_done(self, x):
214 def _engine_done(self, x):
197 self.waitingForEngine = False
215 self.waitingForEngine = False
198 self.didChangeValueForKey_('commandHistory')
216 self.didChangeValueForKey_('commandHistory')
199 return x
217 return x
200
218
201 def _update_user_ns(self, result):
219 def _update_user_ns(self, result):
202 """Update self.userNS from self.engine's namespace"""
220 """Update self.userNS from self.engine's namespace"""
203 d = self.engine.keys()
221 d = self.engine.keys()
204 d.addCallback(self._get_engine_namespace_values_for_keys)
222 d.addCallback(self._get_engine_namespace_values_for_keys)
205
223
206 return result
224 return result
207
225
208
226
209 def _get_engine_namespace_values_for_keys(self, keys):
227 def _get_engine_namespace_values_for_keys(self, keys):
210 d = self.engine.pull(keys)
228 d = self.engine.pull(keys)
211 d.addCallback(self._store_engine_namespace_values, keys=keys)
229 d.addCallback(self._store_engine_namespace_values, keys=keys)
212
230
213
231
214 def _store_engine_namespace_values(self, values, keys=[]):
232 def _store_engine_namespace_values(self, values, keys=[]):
215 assert(len(values) == len(keys))
233 assert(len(values) == len(keys))
216 self.willChangeValueForKey_('userNS')
234 self.willChangeValueForKey_('userNS')
217 for (k,v) in zip(keys,values):
235 for (k,v) in zip(keys,values):
218 self.userNS[k] = saferepr(v)
236 self.userNS[k] = saferepr(v)
219 self.didChangeValueForKey_('userNS')
237 self.didChangeValueForKey_('userNS')
220
238
221
239
222 def update_cell_prompt(self, result, blockID=None):
240 def update_cell_prompt(self, result, blockID=None):
223 if(isinstance(result, Failure)):
241 if(isinstance(result, Failure)):
224 self.insert_text(self.input_prompt(),
242 self.insert_text(self.input_prompt(),
225 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
243 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
226 scrollToVisible=False
244 scrollToVisible=False
227 )
245 )
228 else:
246 else:
229 self.insert_text(self.input_prompt(number=result['number']),
247 self.insert_text(self.input_prompt(number=result['number']),
230 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
248 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
231 scrollToVisible=False
249 scrollToVisible=False
232 )
250 )
233
251
234 return result
252 return result
235
253
236
254
237 def render_result(self, result):
255 def render_result(self, result):
238 blockID = result['blockID']
256 blockID = result['blockID']
239 inputRange = self.blockRanges[blockID]
257 inputRange = self.blockRanges[blockID]
240 del self.blockRanges[blockID]
258 del self.blockRanges[blockID]
241
259
242 #print inputRange,self.current_block_range()
260 #print inputRange,self.current_block_range()
243 self.insert_text('\n' +
261 self.insert_text('\n' +
244 self.output_prompt(result) +
262 self.output_prompt(number=result['number']) +
245 result.get('display',{}).get('pprint','') +
263 result.get('display',{}).get('pprint','') +
246 '\n\n',
264 '\n\n',
247 textRange=NSMakeRange(inputRange.location+inputRange.length,
265 textRange=NSMakeRange(inputRange.location+inputRange.length,
248 0))
266 0))
249 return result
267 return result
250
268
251
269
252 def render_error(self, failure):
270 def render_error(self, failure):
253 self.insert_text('\n\n'+str(failure)+'\n\n')
271 self.insert_text('\n' +
272 self.output_prompt() +
273 '\n' +
274 failure.getErrorMessage() +
275 '\n\n')
254 self.start_new_block()
276 self.start_new_block()
255 return failure
277 return failure
256
278
257
279
258 def _start_cli_banner(self):
280 def _start_cli_banner(self):
259 """Print banner"""
281 """Print banner"""
260
282
261 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
283 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
262 IPython.__version__
284 IPython.__version__
263
285
264 self.insert_text(banner + '\n\n')
286 self.insert_text(banner + '\n\n')
265
287
266
288
267 def start_new_block(self):
289 def start_new_block(self):
268 """"""
290 """"""
269
291
270 self.currentBlockID = self.next_block_ID()
292 self.currentBlockID = self.next_block_ID()
271
293
272
294
273
295
274 def next_block_ID(self):
296 def next_block_ID(self):
275
297
276 return uuid.uuid4()
298 return uuid.uuid4()
277
299
278 def current_block_range(self):
300 def current_block_range(self):
279 return self.blockRanges.get(self.currentBlockID,
301 return self.blockRanges.get(self.currentBlockID,
280 NSMakeRange(self.textView.textStorage().length(),
302 NSMakeRange(self.textView.textStorage().length(),
281 0))
303 0))
282
304
283 def current_block(self):
305 def current_block(self):
284 """The current block's text"""
306 """The current block's text"""
285
307
286 return self.text_for_range(self.current_block_range())
308 return self.text_for_range(self.current_block_range())
287
309
288 def text_for_range(self, textRange):
310 def text_for_range(self, textRange):
289 """text_for_range"""
311 """text_for_range"""
290
312
291 ts = self.textView.textStorage()
313 ts = self.textView.textStorage()
292 return ts.string().substringWithRange_(textRange)
314 return ts.string().substringWithRange_(textRange)
293
315
294 def current_line(self):
316 def current_line(self):
295 block = self.text_for_range(self.current_block_range())
317 block = self.text_for_range(self.current_block_range())
296 block = block.split('\n')
318 block = block.split('\n')
297 return block[-1]
319 return block[-1]
298
320
299
321
300 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
322 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
301 """Insert text into textView at textRange, updating blockRanges
323 """Insert text into textView at textRange, updating blockRanges
302 as necessary
324 as necessary
303 """
325 """
304
326
305 if(textRange == None):
327 if(textRange == None):
306 #range for end of text
328 #range for end of text
307 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
329 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
308
330
309 for r in self.blockRanges.itervalues():
331 for r in self.blockRanges.itervalues():
310 intersection = NSIntersectionRange(r,textRange)
332 intersection = NSIntersectionRange(r,textRange)
311 if(intersection.length == 0): #ranges don't intersect
333 if(intersection.length == 0): #ranges don't intersect
312 if r.location >= textRange.location:
334 if r.location >= textRange.location:
313 r.location += len(string)
335 r.location += len(string)
314 else: #ranges intersect
336 else: #ranges intersect
315 if(r.location <= textRange.location):
337 if(r.location <= textRange.location):
316 assert(intersection.length == textRange.length)
338 assert(intersection.length == textRange.length)
317 r.length += textRange.length
339 r.length += textRange.length
318 else:
340 else:
319 r.location += intersection.length
341 r.location += intersection.length
320
342
321 self.textView.replaceCharactersInRange_withString_(
343 self.textView.replaceCharactersInRange_withString_(
322 textRange, string)
344 textRange, string)
323 self.textView.setSelectedRange_(
345 self.textView.setSelectedRange_(
324 NSMakeRange(textRange.location+len(string), 0))
346 NSMakeRange(textRange.location+len(string), 0))
325 if(scrollToVisible):
347 if(scrollToVisible):
326 self.textView.scrollRangeToVisible_(textRange)
348 self.textView.scrollRangeToVisible_(textRange)
327
349
328
350
329
351
330
352
331 def replace_current_block_with_string(self, textView, string):
353 def replace_current_block_with_string(self, textView, string):
332 textView.replaceCharactersInRange_withString_(
354 textView.replaceCharactersInRange_withString_(
333 self.current_block_range(),
355 self.current_block_range(),
334 string)
356 string)
335 self.current_block_range().length = len(string)
357 self.current_block_range().length = len(string)
336 r = NSMakeRange(textView.textStorage().length(), 0)
358 r = NSMakeRange(textView.textStorage().length(), 0)
337 textView.scrollRangeToVisible_(r)
359 textView.scrollRangeToVisible_(r)
338 textView.setSelectedRange_(r)
360 textView.setSelectedRange_(r)
339
361
340
362
341 def current_indent_string(self):
363 def current_indent_string(self):
342 """returns string for indent or None if no indent"""
364 """returns string for indent or None if no indent"""
343
365
344 return self._indent_for_block(self.current_block())
366 return self._indent_for_block(self.current_block())
345
367
346
368
347 def _indent_for_block(self, block):
369 def _indent_for_block(self, block):
348 lines = block.split('\n')
370 lines = block.split('\n')
349 if(len(lines) > 1):
371 if(len(lines) > 1):
350 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
372 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
351 if(currentIndent == 0):
373 if(currentIndent == 0):
352 currentIndent = self.tabSpaces
374 currentIndent = self.tabSpaces
353
375
354 if(self.tabUsesSpaces):
376 if(self.tabUsesSpaces):
355 result = ' ' * currentIndent
377 result = ' ' * currentIndent
356 else:
378 else:
357 result = '\t' * (currentIndent/self.tabSpaces)
379 result = '\t' * (currentIndent/self.tabSpaces)
358 else:
380 else:
359 result = None
381 result = None
360
382
361 return result
383 return result
362
384
363
385
364 # NSTextView delegate methods...
386 # NSTextView delegate methods...
365 def textView_doCommandBySelector_(self, textView, selector):
387 def textView_doCommandBySelector_(self, textView, selector):
366 assert(textView == self.textView)
388 assert(textView == self.textView)
367 NSLog("textView_doCommandBySelector_: "+selector)
389 NSLog("textView_doCommandBySelector_: "+selector)
368
390
369
391
370 if(selector == 'insertNewline:'):
392 if(selector == 'insertNewline:'):
371 indent = self.current_indent_string()
393 indent = self.current_indent_string()
372 if(indent):
394 if(indent):
373 line = indent + self.current_line()
395 line = indent + self.current_line()
374 else:
396 else:
375 line = self.current_line()
397 line = self.current_line()
376
398
377 if(self.is_complete(self.current_block())):
399 if(self.is_complete(self.current_block())):
378 self.execute(self.current_block(),
400 self.execute(self.current_block(),
379 blockID=self.currentBlockID)
401 blockID=self.currentBlockID)
380 self.start_new_block()
402 self.start_new_block()
381
403
382 return True
404 return True
383
405
384 return False
406 return False
385
407
386 elif(selector == 'moveUp:'):
408 elif(selector == 'moveUp:'):
387 prevBlock = self.get_history_previous(self.current_block())
409 prevBlock = self.get_history_previous(self.current_block())
388 if(prevBlock != None):
410 if(prevBlock != None):
389 self.replace_current_block_with_string(textView, prevBlock)
411 self.replace_current_block_with_string(textView, prevBlock)
390 else:
412 else:
391 NSBeep()
413 NSBeep()
392 return True
414 return True
393
415
394 elif(selector == 'moveDown:'):
416 elif(selector == 'moveDown:'):
395 nextBlock = self.get_history_next()
417 nextBlock = self.get_history_next()
396 if(nextBlock != None):
418 if(nextBlock != None):
397 self.replace_current_block_with_string(textView, nextBlock)
419 self.replace_current_block_with_string(textView, nextBlock)
398 else:
420 else:
399 NSBeep()
421 NSBeep()
400 return True
422 return True
401
423
402 elif(selector == 'moveToBeginningOfParagraph:'):
424 elif(selector == 'moveToBeginningOfParagraph:'):
403 textView.setSelectedRange_(NSMakeRange(
425 textView.setSelectedRange_(NSMakeRange(
404 self.current_block_range().location,
426 self.current_block_range().location,
405 0))
427 0))
406 return True
428 return True
407 elif(selector == 'moveToEndOfParagraph:'):
429 elif(selector == 'moveToEndOfParagraph:'):
408 textView.setSelectedRange_(NSMakeRange(
430 textView.setSelectedRange_(NSMakeRange(
409 self.current_block_range().location + \
431 self.current_block_range().location + \
410 self.current_block_range().length, 0))
432 self.current_block_range().length, 0))
411 return True
433 return True
412 elif(selector == 'deleteToEndOfParagraph:'):
434 elif(selector == 'deleteToEndOfParagraph:'):
413 if(textView.selectedRange().location <= \
435 if(textView.selectedRange().location <= \
414 self.current_block_range().location):
436 self.current_block_range().location):
415 # Intersect the selected range with the current line range
437 # Intersect the selected range with the current line range
416 if(self.current_block_range().length < 0):
438 if(self.current_block_range().length < 0):
417 self.blockRanges[self.currentBlockID].length = 0
439 self.blockRanges[self.currentBlockID].length = 0
418
440
419 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
441 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
420 self.current_block_range())
442 self.current_block_range())
421
443
422 if(r.length > 0): #no intersection
444 if(r.length > 0): #no intersection
423 textView.setSelectedRange_(r)
445 textView.setSelectedRange_(r)
424
446
425 return False # don't actually handle the delete
447 return False # don't actually handle the delete
426
448
427 elif(selector == 'insertTab:'):
449 elif(selector == 'insertTab:'):
428 if(len(self.current_line().strip()) == 0): #only white space
450 if(len(self.current_line().strip()) == 0): #only white space
429 return False
451 return False
430 else:
452 else:
431 self.textView.complete_(self)
453 self.textView.complete_(self)
432 return True
454 return True
433
455
434 elif(selector == 'deleteBackward:'):
456 elif(selector == 'deleteBackward:'):
435 #if we're at the beginning of the current block, ignore
457 #if we're at the beginning of the current block, ignore
436 if(textView.selectedRange().location == \
458 if(textView.selectedRange().location == \
437 self.current_block_range().location):
459 self.current_block_range().location):
438 return True
460 return True
439 else:
461 else:
440 self.current_block_range().length-=1
462 self.current_block_range().length-=1
441 return False
463 return False
442 return False
464 return False
443
465
444
466
445 def textView_shouldChangeTextInRanges_replacementStrings_(self,
467 def textView_shouldChangeTextInRanges_replacementStrings_(self,
446 textView, ranges, replacementStrings):
468 textView, ranges, replacementStrings):
447 """
469 """
448 Delegate method for NSTextView.
470 Delegate method for NSTextView.
449
471
450 Refuse change text in ranges not at end, but make those changes at
472 Refuse change text in ranges not at end, but make those changes at
451 end.
473 end.
452 """
474 """
453
475
454 assert(len(ranges) == len(replacementStrings))
476 assert(len(ranges) == len(replacementStrings))
455 allow = True
477 allow = True
456 for r,s in zip(ranges, replacementStrings):
478 for r,s in zip(ranges, replacementStrings):
457 r = r.rangeValue()
479 r = r.rangeValue()
458 if(textView.textStorage().length() > 0 and
480 if(textView.textStorage().length() > 0 and
459 r.location < self.current_block_range().location):
481 r.location < self.current_block_range().location):
460 self.insert_text(s)
482 self.insert_text(s)
461 allow = False
483 allow = False
462
484
463
485
464 self.blockRanges.setdefault(self.currentBlockID,
486 self.blockRanges.setdefault(self.currentBlockID,
465 self.current_block_range()).length +=\
487 self.current_block_range()).length +=\
466 len(s)
488 len(s)
467
489
468 return allow
490 return allow
469
491
470 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
492 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
471 textView, words, charRange, index):
493 textView, words, charRange, index):
472 try:
494 try:
473 ts = textView.textStorage()
495 ts = textView.textStorage()
474 token = ts.string().substringWithRange_(charRange)
496 token = ts.string().substringWithRange_(charRange)
475 completions = blockingCallFromThread(self.complete, token)
497 completions = blockingCallFromThread(self.complete, token)
476 except:
498 except:
477 completions = objc.nil
499 completions = objc.nil
478 NSBeep()
500 NSBeep()
479
501
480 return (completions,0)
502 return (completions,0)
481
503
482
504
@@ -1,349 +1,349 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
3 """
3 """
4 frontendbase provides an interface and base class for GUI frontends for
4 frontendbase provides an interface and base class for GUI frontends for
5 IPython.kernel/IPython.kernel.core.
5 IPython.kernel/IPython.kernel.core.
6
6
7 Frontend implementations will likely want to subclass FrontEndBase.
7 Frontend implementations will likely want to subclass FrontEndBase.
8
8
9 Author: Barry Wark
9 Author: Barry Wark
10 """
10 """
11 __docformat__ = "restructuredtext en"
11 __docformat__ = "restructuredtext en"
12
12
13 #-------------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
14 # Copyright (C) 2008 The IPython Development Team
14 # Copyright (C) 2008 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-------------------------------------------------------------------------------
18 #-------------------------------------------------------------------------------
19
19
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-------------------------------------------------------------------------------
22 #-------------------------------------------------------------------------------
23 import string
23 import string
24 import uuid
24 import uuid
25 import _ast
25 import _ast
26
26
27 import zope.interface as zi
27 import zope.interface as zi
28
28
29 from IPython.kernel.core.history import FrontEndHistory
29 from IPython.kernel.core.history import FrontEndHistory
30 from IPython.kernel.core.util import Bunch
30 from IPython.kernel.core.util import Bunch
31 from IPython.kernel.engineservice import IEngineCore
31 from IPython.kernel.engineservice import IEngineCore
32
32
33 from twisted.python.failure import Failure
33 from twisted.python.failure import Failure
34
34
35 ##############################################################################
35 ##############################################################################
36 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
36 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
37 # not
37 # not
38
38
39 rc = Bunch()
39 rc = Bunch()
40 rc.prompt_in1 = r'In [$number]: '
40 rc.prompt_in1 = r'In [$number]: '
41 rc.prompt_in2 = r'...'
41 rc.prompt_in2 = r'...'
42 rc.prompt_out = r'Out [$number]: '
42 rc.prompt_out = r'Out [$number]: '
43
43
44 ##############################################################################
44 ##############################################################################
45
45
46 class IFrontEndFactory(zi.Interface):
46 class IFrontEndFactory(zi.Interface):
47 """Factory interface for frontends."""
47 """Factory interface for frontends."""
48
48
49 def __call__(engine=None, history=None):
49 def __call__(engine=None, history=None):
50 """
50 """
51 Parameters:
51 Parameters:
52 interpreter : IPython.kernel.engineservice.IEngineCore
52 interpreter : IPython.kernel.engineservice.IEngineCore
53 """
53 """
54
54
55 pass
55 pass
56
56
57
57
58
58
59 class IFrontEnd(zi.Interface):
59 class IFrontEnd(zi.Interface):
60 """Interface for frontends. All methods return t.i.d.Deferred"""
60 """Interface for frontends. All methods return t.i.d.Deferred"""
61
61
62 zi.Attribute("input_prompt_template", "string.Template instance\
62 zi.Attribute("input_prompt_template", "string.Template instance\
63 substituteable with execute result.")
63 substituteable with execute result.")
64 zi.Attribute("output_prompt_template", "string.Template instance\
64 zi.Attribute("output_prompt_template", "string.Template instance\
65 substituteable with execute result.")
65 substituteable with execute result.")
66 zi.Attribute("continuation_prompt_template", "string.Template instance\
66 zi.Attribute("continuation_prompt_template", "string.Template instance\
67 substituteable with execute result.")
67 substituteable with execute result.")
68
68
69 def update_cell_prompt(result, blockID=None):
69 def update_cell_prompt(result, blockID=None):
70 """Subclass may override to update the input prompt for a block.
70 """Subclass may override to update the input prompt for a block.
71 Since this method will be called as a
71 Since this method will be called as a
72 twisted.internet.defer.Deferred's callback/errback,
72 twisted.internet.defer.Deferred's callback/errback,
73 implementations should return result when finished.
73 implementations should return result when finished.
74
74
75 Result is a result dict in case of success, and a
75 Result is a result dict in case of success, and a
76 twisted.python.util.failure.Failure in case of an error
76 twisted.python.util.failure.Failure in case of an error
77 """
77 """
78
78
79 pass
79 pass
80
80
81
81
82 def render_result(result):
82 def render_result(result):
83 """Render the result of an execute call. Implementors may choose the
83 """Render the result of an execute call. Implementors may choose the
84 method of rendering.
84 method of rendering.
85 For example, a notebook-style frontend might render a Chaco plot
85 For example, a notebook-style frontend might render a Chaco plot
86 inline.
86 inline.
87
87
88 Parameters:
88 Parameters:
89 result : dict (result of IEngineBase.execute )
89 result : dict (result of IEngineBase.execute )
90 blockID = result['blockID']
90 blockID = result['blockID']
91
91
92 Result:
92 Result:
93 Output of frontend rendering
93 Output of frontend rendering
94 """
94 """
95
95
96 pass
96 pass
97
97
98 def render_error(failure):
98 def render_error(failure):
99 """Subclasses must override to render the failure. Since this method
99 """Subclasses must override to render the failure. Since this method
100 will be called as a twisted.internet.defer.Deferred's callback,
100 will be called as a twisted.internet.defer.Deferred's callback,
101 implementations should return result when finished.
101 implementations should return result when finished.
102
102
103 blockID = failure.blockID
103 blockID = failure.blockID
104 """
104 """
105
105
106 pass
106 pass
107
107
108
108
109 def input_prompt(number=None):
109 def input_prompt(number=''):
110 """Returns the input prompt by subsituting into
110 """Returns the input prompt by subsituting into
111 self.input_prompt_template
111 self.input_prompt_template
112 """
112 """
113 pass
113 pass
114
114
115 def output_prompt(number=None):
115 def output_prompt(number=''):
116 """Returns the output prompt by subsituting into
116 """Returns the output prompt by subsituting into
117 self.output_prompt_template
117 self.output_prompt_template
118 """
118 """
119
119
120 pass
120 pass
121
121
122 def continuation_prompt():
122 def continuation_prompt():
123 """Returns the continuation prompt by subsituting into
123 """Returns the continuation prompt by subsituting into
124 self.continuation_prompt_template
124 self.continuation_prompt_template
125 """
125 """
126
126
127 pass
127 pass
128
128
129 def is_complete(block):
129 def is_complete(block):
130 """Returns True if block is complete, False otherwise."""
130 """Returns True if block is complete, False otherwise."""
131
131
132 pass
132 pass
133
133
134 def compile_ast(block):
134 def compile_ast(block):
135 """Compiles block to an _ast.AST"""
135 """Compiles block to an _ast.AST"""
136
136
137 pass
137 pass
138
138
139
139
140 def get_history_previous(currentBlock):
140 def get_history_previous(currentBlock):
141 """Returns the block previous in the history. Saves currentBlock if
141 """Returns the block previous in the history. Saves currentBlock if
142 the history_cursor is currently at the end of the input history"""
142 the history_cursor is currently at the end of the input history"""
143 pass
143 pass
144
144
145 def get_history_next():
145 def get_history_next():
146 """Returns the next block in the history."""
146 """Returns the next block in the history."""
147
147
148 pass
148 pass
149
149
150
150
151 class FrontEndBase(object):
151 class FrontEndBase(object):
152 """
152 """
153 FrontEndBase manages the state tasks for a CLI frontend:
153 FrontEndBase manages the state tasks for a CLI frontend:
154 - Input and output history management
154 - Input and output history management
155 - Input/continuation and output prompt generation
155 - Input/continuation and output prompt generation
156
156
157 Some issues (due to possibly unavailable engine):
157 Some issues (due to possibly unavailable engine):
158 - How do we get the current cell number for the engine?
158 - How do we get the current cell number for the engine?
159 - How do we handle completions?
159 - How do we handle completions?
160 """
160 """
161
161
162 zi.implements(IFrontEnd)
162 zi.implements(IFrontEnd)
163 zi.classProvides(IFrontEndFactory)
163 zi.classProvides(IFrontEndFactory)
164
164
165 history_cursor = 0
165 history_cursor = 0
166
166
167 current_indent_level = 0
167 current_indent_level = 0
168
168
169
169
170 input_prompt_template = string.Template(rc.prompt_in1)
170 input_prompt_template = string.Template(rc.prompt_in1)
171 output_prompt_template = string.Template(rc.prompt_out)
171 output_prompt_template = string.Template(rc.prompt_out)
172 continuation_prompt_template = string.Template(rc.prompt_in2)
172 continuation_prompt_template = string.Template(rc.prompt_in2)
173
173
174 def __init__(self, engine=None, history=None):
174 def __init__(self, engine=None, history=None):
175 assert(engine==None or IEngineCore.providedBy(engine))
175 assert(engine==None or IEngineCore.providedBy(engine))
176 self.engine = IEngineCore(engine)
176 self.engine = IEngineCore(engine)
177 if history is None:
177 if history is None:
178 self.history = FrontEndHistory(input_cache=[''])
178 self.history = FrontEndHistory(input_cache=[''])
179 else:
179 else:
180 self.history = history
180 self.history = history
181
181
182
182
183 def input_prompt(self, number=None):
183 def input_prompt(self, number=''):
184 """Returns the current input prompt
184 """Returns the current input prompt
185
185
186 It would be great to use ipython1.core.prompts.Prompt1 here
186 It would be great to use ipython1.core.prompts.Prompt1 here
187 """
187 """
188 return self.input_prompt_template.safe_substitute({'number':number})
188 return self.input_prompt_template.safe_substitute({'number':number})
189
189
190
190
191 def continuation_prompt(self):
191 def continuation_prompt(self):
192 """Returns the current continuation prompt"""
192 """Returns the current continuation prompt"""
193
193
194 return self.continuation_prompt_template.safe_substitute()
194 return self.continuation_prompt_template.safe_substitute()
195
195
196 def output_prompt(self, number=None):
196 def output_prompt(self, number=''):
197 """Returns the output prompt for result"""
197 """Returns the output prompt for result"""
198
198
199 return self.output_prompt_template.safe_substitute({'number':number})
199 return self.output_prompt_template.safe_substitute({'number':number})
200
200
201
201
202 def is_complete(self, block):
202 def is_complete(self, block):
203 """Determine if block is complete.
203 """Determine if block is complete.
204
204
205 Parameters
205 Parameters
206 block : string
206 block : string
207
207
208 Result
208 Result
209 True if block can be sent to the engine without compile errors.
209 True if block can be sent to the engine without compile errors.
210 False otherwise.
210 False otherwise.
211 """
211 """
212
212
213 try:
213 try:
214 ast = self.compile_ast(block)
214 ast = self.compile_ast(block)
215 except:
215 except:
216 return False
216 return False
217
217
218 lines = block.split('\n')
218 lines = block.split('\n')
219 return (len(lines)==1 or str(lines[-1])=='')
219 return (len(lines)==1 or str(lines[-1])=='')
220
220
221
221
222 def compile_ast(self, block):
222 def compile_ast(self, block):
223 """Compile block to an AST
223 """Compile block to an AST
224
224
225 Parameters:
225 Parameters:
226 block : str
226 block : str
227
227
228 Result:
228 Result:
229 AST
229 AST
230
230
231 Throws:
231 Throws:
232 Exception if block cannot be compiled
232 Exception if block cannot be compiled
233 """
233 """
234
234
235 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
235 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
236
236
237
237
238 def execute(self, block, blockID=None):
238 def execute(self, block, blockID=None):
239 """Execute the block and return result.
239 """Execute the block and return result.
240
240
241 Parameters:
241 Parameters:
242 block : {str, AST}
242 block : {str, AST}
243 blockID : any
243 blockID : any
244 Caller may provide an ID to identify this block.
244 Caller may provide an ID to identify this block.
245 result['blockID'] := blockID
245 result['blockID'] := blockID
246
246
247 Result:
247 Result:
248 Deferred result of self.interpreter.execute
248 Deferred result of self.interpreter.execute
249 """
249 """
250
250
251 if(not self.is_complete(block)):
251 if(not self.is_complete(block)):
252 return Failure(Exception("Block is not compilable"))
252 return Failure(Exception("Block is not compilable"))
253
253
254 if(blockID == None):
254 if(blockID == None):
255 blockID = uuid.uuid4() #random UUID
255 blockID = uuid.uuid4() #random UUID
256
256
257 d = self.engine.execute(block)
257 d = self.engine.execute(block)
258 d.addCallback(self._add_history, block=block)
258 d.addCallback(self._add_history, block=block)
259 d.addCallbacks(self._add_block_id_for_result,
259 d.addCallbacks(self._add_block_id_for_result,
260 errback=self._add_block_id_for_failure,
260 errback=self._add_block_id_for_failure,
261 callbackArgs=(blockID,),
261 callbackArgs=(blockID,),
262 errbackArgs=(blockID,))
262 errbackArgs=(blockID,))
263 d.addBoth(self.update_cell_prompt, blockID=blockID)
263 d.addBoth(self.update_cell_prompt, blockID=blockID)
264 d.addCallbacks(self.render_result,
264 d.addCallbacks(self.render_result,
265 errback=self.render_error)
265 errback=self.render_error)
266
266
267 return d
267 return d
268
268
269
269
270 def _add_block_id_for_result(self, result, blockID):
270 def _add_block_id_for_result(self, result, blockID):
271 """Add the blockID to result or failure. Unfortunatley, we have to
271 """Add the blockID to result or failure. Unfortunatley, we have to
272 treat failures differently than result dicts.
272 treat failures differently than result dicts.
273 """
273 """
274
274
275 result['blockID'] = blockID
275 result['blockID'] = blockID
276
276
277 return result
277 return result
278
278
279 def _add_block_id_for_failure(self, failure, blockID):
279 def _add_block_id_for_failure(self, failure, blockID):
280 """_add_block_id_for_failure"""
280 """_add_block_id_for_failure"""
281
281
282 failure.blockID = blockID
282 failure.blockID = blockID
283 return failure
283 return failure
284
284
285
285
286 def _add_history(self, result, block=None):
286 def _add_history(self, result, block=None):
287 """Add block to the history"""
287 """Add block to the history"""
288
288
289 assert(block != None)
289 assert(block != None)
290 self.history.add_items([block])
290 self.history.add_items([block])
291 self.history_cursor += 1
291 self.history_cursor += 1
292
292
293 return result
293 return result
294
294
295
295
296 def get_history_previous(self, currentBlock):
296 def get_history_previous(self, currentBlock):
297 """ Returns previous history string and decrement history cursor.
297 """ Returns previous history string and decrement history cursor.
298 """
298 """
299 command = self.history.get_history_item(self.history_cursor - 1)
299 command = self.history.get_history_item(self.history_cursor - 1)
300
300
301 if command is not None:
301 if command is not None:
302 if(self.history_cursor == len(self.history.input_cache)):
302 if(self.history_cursor == len(self.history.input_cache)):
303 self.history.input_cache[self.history_cursor] = currentBlock
303 self.history.input_cache[self.history_cursor] = currentBlock
304 self.history_cursor -= 1
304 self.history_cursor -= 1
305 return command
305 return command
306
306
307
307
308 def get_history_next(self):
308 def get_history_next(self):
309 """ Returns next history string and increment history cursor.
309 """ Returns next history string and increment history cursor.
310 """
310 """
311 command = self.history.get_history_item(self.history_cursor+1)
311 command = self.history.get_history_item(self.history_cursor+1)
312
312
313 if command is not None:
313 if command is not None:
314 self.history_cursor += 1
314 self.history_cursor += 1
315 return command
315 return command
316
316
317 ###
317 ###
318 # Subclasses probably want to override these methods...
318 # Subclasses probably want to override these methods...
319 ###
319 ###
320
320
321 def update_cell_prompt(self, result, blockID=None):
321 def update_cell_prompt(self, result, blockID=None):
322 """Subclass may override to update the input prompt for a block.
322 """Subclass may override to update the input prompt for a block.
323 Since this method will be called as a
323 Since this method will be called as a
324 twisted.internet.defer.Deferred's callback, implementations should
324 twisted.internet.defer.Deferred's callback, implementations should
325 return result when finished.
325 return result when finished.
326 """
326 """
327
327
328 return result
328 return result
329
329
330
330
331 def render_result(self, result):
331 def render_result(self, result):
332 """Subclasses must override to render result. Since this method will
332 """Subclasses must override to render result. Since this method will
333 be called as a twisted.internet.defer.Deferred's callback,
333 be called as a twisted.internet.defer.Deferred's callback,
334 implementations should return result when finished.
334 implementations should return result when finished.
335 """
335 """
336
336
337 return result
337 return result
338
338
339
339
340 def render_error(self, failure):
340 def render_error(self, failure):
341 """Subclasses must override to render the failure. Since this method
341 """Subclasses must override to render the failure. Since this method
342 will be called as a twisted.internet.defer.Deferred's callback,
342 will be called as a twisted.internet.defer.Deferred's callback,
343 implementations should return result when finished.
343 implementations should return result when finished.
344 """
344 """
345
345
346 return failure
346 return failure
347
347
348
348
349
349
General Comments 0
You need to be logged in to leave comments. Login now