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