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