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