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