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