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