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