##// END OF EJS Templates
merged from trunk. cocoa_frontend refactored to keep input prompt and input ranges. no crash, but garbage output
Barry Wark -
r1322:e14c46f7 merge
parent child Browse files
Show More
@@ -1,479 +1,548 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
2 # -*- test-case-name: IPython.frontend.cocoa.tests.test_cocoa_frontend -*-
3
3
4 """PyObjC classes to provide a Cocoa frontend to the
4 """PyObjC classes to provide a Cocoa frontend to the
5 IPython.kernel.engineservice.IEngineBase.
5 IPython.kernel.engineservice.IEngineBase.
6
6
7 To add an IPython interpreter to a cocoa app, instantiate an
7 To add an IPython interpreter to a cocoa app, instantiate an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
8 IPythonCocoaController in a XIB and connect its textView outlet to an
9 NSTextView instance in your UI. That's it.
9 NSTextView instance in your UI. That's it.
10
10
11 Author: Barry Wark
11 Author: Barry Wark
12 """
12 """
13
13
14 __docformat__ = "restructuredtext en"
14 __docformat__ = "restructuredtext en"
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Copyright (C) 2008 The IPython Development Team
17 # Copyright (C) 2008 The IPython Development Team
18 #
18 #
19 # Distributed under the terms of the BSD License. The full license is in
19 # Distributed under the terms of the BSD License. The full license is in
20 # the file COPYING, distributed as part of this software.
20 # the file COPYING, distributed as part of this software.
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Imports
24 # Imports
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 import sys
27 import sys
28 import objc
28 import objc
29 import uuid
29 import uuid
30
30
31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
31 from Foundation import NSObject, NSMutableArray, NSMutableDictionary,\
32 NSLog, NSNotificationCenter, NSMakeRange,\
32 NSLog, NSNotificationCenter, NSMakeRange,\
33 NSLocalizedString, NSIntersectionRange,\
33 NSLocalizedString, NSIntersectionRange,\
34 NSString, NSAutoreleasePool
34 NSString, NSAutoreleasePool
35
35
36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
36 from AppKit import NSApplicationWillTerminateNotification, NSBeep,\
37 NSTextView, NSRulerView, NSVerticalRuler
37 NSTextView, NSRulerView, NSVerticalRuler
38
38
39 from pprint import saferepr
39 from pprint import saferepr
40
40
41 import IPython
41 import IPython
42 from IPython.kernel.engineservice import ThreadedEngineService
42 from IPython.kernel.engineservice import ThreadedEngineService
43 from IPython.frontend.frontendbase import AsyncFrontEndBase
43 from IPython.frontend.frontendbase import AsyncFrontEndBase
44
44
45 from twisted.internet.threads import blockingCallFromThread
45 from twisted.internet.threads import blockingCallFromThread
46 from twisted.python.failure import Failure
46 from twisted.python.failure import Failure
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Classes to implement the Cocoa frontend
49 # Classes to implement the Cocoa frontend
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 # TODO:
52 # TODO:
53 # 1. use MultiEngineClient and out-of-process engine rather than
53 # 1. use MultiEngineClient and out-of-process engine rather than
54 # ThreadedEngineService?
54 # ThreadedEngineService?
55 # 2. integrate Xgrid launching of engines
55 # 2. integrate Xgrid launching of engines
56
56
57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
57 class AutoreleasePoolWrappedThreadedEngineService(ThreadedEngineService):
58 """Wrap all blocks in an NSAutoreleasePool"""
58 """Wrap all blocks in an NSAutoreleasePool"""
59
59
60 def wrapped_execute(self, msg, lines):
60 def wrapped_execute(self, msg, lines):
61 """wrapped_execute"""
61 """wrapped_execute"""
62 try:
62 try:
63 p = NSAutoreleasePool.alloc().init()
63 p = NSAutoreleasePool.alloc().init()
64 result = super(AutoreleasePoolWrappedThreadedEngineService,
64 result = super(AutoreleasePoolWrappedThreadedEngineService,
65 self).wrapped_execute(msg, lines)
65 self).wrapped_execute(msg, lines)
66 finally:
66 finally:
67 p.drain()
67 p.drain()
68
68
69 return result
69 return result
70
70
71
71
72
72
73 class CellBlock(object):
74 """
75 Storage for information about text ranges relating to a single cell
76 """
77
78
79 def __init__(self, inputPromptRange, inputRange=None, outputPromptRange=None,
80 outputRange=None):
81 super(CellBlock, self).__init__()
82 self.inputPromptRange = inputPromptRange
83 self.inputRange = inputRange
84 self.outputPromptRange = outputPromptRange
85 self.outputRange = outputRange
86
87 def update_ranges_for_insertion(self, text, textRange):
88 """Update ranges for text insertion at textRange"""
89
90 for r in [self.inputPromptRange,self.inputRange,
91 self.outputPromptRange, self.outputRange]:
92 if(r == None):
93 continue
94 intersection = NSIntersectionRange(r,textRange)
95 if(intersection.length == 0): #ranges don't intersect
96 if r.location >= textRange.location:
97 r.location += len(text)
98 else: #ranges intersect
99 if(r.location > textRange.location):
100 offset = len(text) - intersection.length
101 r.length -= offset
102 r.location += offset
103 elif(r.location == textRange.location):
104 r.length += len(text) - intersection.length
105 else:
106 r.length -= intersection.length
107
108
109 def update_ranges_for_deletion(self, textRange):
110 """Update ranges for text deletion at textRange"""
111
112 for r in [self.inputPromptRange,self.inputRange,
113 self.outputPromptRange, self.outputRange]:
114 if(r==None):
115 continue
116 intersection = NSIntersectionRange(r, textRange)
117 if(intersection.length == 0): #ranges don't intersect
118 if r.location >= textRange.location:
119 r.location -= textRange.length
120 else: #ranges intersect
121 if(r.location > textRange.location):
122 offset = intersection.length
123 r.length -= offset
124 r.location += offset
125 elif(r.location == textRange.location):
126 r.length += intersection.length
127 else:
128 r.length -= intersection.length
129
130 def __repr__(self):
131 return 'CellBlock('+ str((self.inputPromptRange,
132 self.inputRange,
133 self.outputPromptRange,
134 self.outputRange)) + ')'
135
136
137
138
73 class IPythonCocoaController(NSObject, AsyncFrontEndBase):
139 class IPythonCocoaController(NSObject, AsyncFrontEndBase):
74 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
140 userNS = objc.ivar() #mirror of engine.user_ns (key=>str(value))
75 waitingForEngine = objc.ivar().bool()
141 waitingForEngine = objc.ivar().bool()
76 textView = objc.IBOutlet()
142 textView = objc.IBOutlet()
77
143
78 def init(self):
144 def init(self):
79 self = super(IPythonCocoaController, self).init()
145 self = super(IPythonCocoaController, self).init()
80 AsyncFrontEndBase.__init__(self,
146 AsyncFrontEndBase.__init__(self,
81 engine=AutoreleasePoolWrappedThreadedEngineService())
147 engine=AutoreleasePoolWrappedThreadedEngineService())
82 if(self != None):
148 if(self != None):
83 self._common_init()
149 self._common_init()
84
150
85 return self
151 return self
86
152
87 def _common_init(self):
153 def _common_init(self):
88 """_common_init"""
154 """_common_init"""
89
155
90 self.userNS = NSMutableDictionary.dictionary()
156 self.userNS = NSMutableDictionary.dictionary()
91 self.waitingForEngine = False
157 self.waitingForEngine = False
92
158
93 self.lines = {}
159 self.lines = {}
94 self.tabSpaces = 4
160 self.tabSpaces = 4
95 self.tabUsesSpaces = True
161 self.tabUsesSpaces = True
96 self.currentBlockID = self.next_block_ID()
162 self.currentBlockID = self.next_block_ID()
97 self.blockRanges = {} # blockID=>NSRange
163 self.blockRanges = {} # blockID=>CellBlock
98
164
99
165
100 def awakeFromNib(self):
166 def awakeFromNib(self):
101 """awakeFromNib"""
167 """awakeFromNib"""
102
168
103 self._common_init()
169 self._common_init()
104
170
105 # Start the IPython engine
171 # Start the IPython engine
106 self.engine.startService()
172 self.engine.startService()
107 NSLog('IPython engine started')
173 NSLog('IPython engine started')
108
174
109 # Register for app termination
175 # Register for app termination
110 nc = NSNotificationCenter.defaultCenter()
176 nc = NSNotificationCenter.defaultCenter()
111 nc.addObserver_selector_name_object_(
177 nc.addObserver_selector_name_object_(
112 self,
178 self,
113 'appWillTerminate:',
179 'appWillTerminate:',
114 NSApplicationWillTerminateNotification,
180 NSApplicationWillTerminateNotification,
115 None)
181 None)
116
182
117 self.textView.setDelegate_(self)
183 self.textView.setDelegate_(self)
118 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
184 self.textView.enclosingScrollView().setHasVerticalRuler_(True)
119 r = NSRulerView.alloc().initWithScrollView_orientation_(
185 r = NSRulerView.alloc().initWithScrollView_orientation_(
120 self.textView.enclosingScrollView(),
186 self.textView.enclosingScrollView(),
121 NSVerticalRuler)
187 NSVerticalRuler)
122 self.verticalRulerView = r
188 self.verticalRulerView = r
123 self.verticalRulerView.setClientView_(self.textView)
189 self.verticalRulerView.setClientView_(self.textView)
124 self._start_cli_banner()
190 self._start_cli_banner()
191 self.start_new_block()
125
192
126
193
127 def appWillTerminate_(self, notification):
194 def appWillTerminate_(self, notification):
128 """appWillTerminate"""
195 """appWillTerminate"""
129
196
130 self.engine.stopService()
197 self.engine.stopService()
131
198
132
199
133 def complete(self, token):
200 def complete(self, token):
134 """Complete token in engine's user_ns
201 """Complete token in engine's user_ns
135
202
136 Parameters
203 Parameters
137 ----------
204 ----------
138 token : string
205 token : string
139
206
140 Result
207 Result
141 ------
208 ------
142 Deferred result of
209 Deferred result of
143 IPython.kernel.engineservice.IEngineBase.complete
210 IPython.kernel.engineservice.IEngineBase.complete
144 """
211 """
145
212
146 return self.engine.complete(token)
213 return self.engine.complete(token)
147
214
148
215
149 def execute(self, block, blockID=None):
216 def execute(self, block, blockID=None):
150 self.waitingForEngine = True
217 self.waitingForEngine = True
151 self.willChangeValueForKey_('commandHistory')
218 self.willChangeValueForKey_('commandHistory')
152 d = super(IPythonCocoaController, self).execute(block,
219 d = super(IPythonCocoaController, self).execute(block,
153 blockID)
220 blockID)
154 d.addBoth(self._engine_done)
221 d.addBoth(self._engine_done)
155 d.addCallback(self._update_user_ns)
222 d.addCallback(self._update_user_ns)
156
223
157 return d
224 return d
158
225
159
226
160 def push_(self, namespace):
227 def push_(self, namespace):
161 """Push dictionary of key=>values to python namespace"""
228 """Push dictionary of key=>values to python namespace"""
162
229
163 self.waitingForEngine = True
230 self.waitingForEngine = True
164 self.willChangeValueForKey_('commandHistory')
231 self.willChangeValueForKey_('commandHistory')
165 d = self.engine.push(namespace)
232 d = self.engine.push(namespace)
166 d.addBoth(self._engine_done)
233 d.addBoth(self._engine_done)
167 d.addCallback(self._update_user_ns)
234 d.addCallback(self._update_user_ns)
168
235
169
236
170 def pull_(self, keys):
237 def pull_(self, keys):
171 """Pull keys from python namespace"""
238 """Pull keys from python namespace"""
172
239
173 self.waitingForEngine = True
240 self.waitingForEngine = True
174 result = blockingCallFromThread(self.engine.pull, keys)
241 result = blockingCallFromThread(self.engine.pull, keys)
175 self.waitingForEngine = False
242 self.waitingForEngine = False
176
243
177 @objc.signature('v@:@I')
244 @objc.signature('v@:@I')
178 def executeFileAtPath_encoding_(self, path, encoding):
245 def executeFileAtPath_encoding_(self, path, encoding):
179 """Execute file at path in an empty namespace. Update the engine
246 """Execute file at path in an empty namespace. Update the engine
180 user_ns with the resulting locals."""
247 user_ns with the resulting locals."""
181
248
182 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
249 lines,err = NSString.stringWithContentsOfFile_encoding_error_(
183 path,
250 path,
184 encoding,
251 encoding,
185 None)
252 None)
186 self.engine.execute(lines)
253 self.engine.execute(lines)
187
254
188
255
189 def _engine_done(self, x):
256 def _engine_done(self, x):
190 self.waitingForEngine = False
257 self.waitingForEngine = False
191 self.didChangeValueForKey_('commandHistory')
258 self.didChangeValueForKey_('commandHistory')
192 return x
259 return x
193
260
194 def _update_user_ns(self, result):
261 def _update_user_ns(self, result):
195 """Update self.userNS from self.engine's namespace"""
262 """Update self.userNS from self.engine's namespace"""
196 d = self.engine.keys()
263 d = self.engine.keys()
197 d.addCallback(self._get_engine_namespace_values_for_keys)
264 d.addCallback(self._get_engine_namespace_values_for_keys)
198
265
199 return result
266 return result
200
267
201
268
202 def _get_engine_namespace_values_for_keys(self, keys):
269 def _get_engine_namespace_values_for_keys(self, keys):
203 d = self.engine.pull(keys)
270 d = self.engine.pull(keys)
204 d.addCallback(self._store_engine_namespace_values, keys=keys)
271 d.addCallback(self._store_engine_namespace_values, keys=keys)
205
272
206
273
207 def _store_engine_namespace_values(self, values, keys=[]):
274 def _store_engine_namespace_values(self, values, keys=[]):
208 assert(len(values) == len(keys))
275 assert(len(values) == len(keys))
209 self.willChangeValueForKey_('userNS')
276 self.willChangeValueForKey_('userNS')
210 for (k,v) in zip(keys,values):
277 for (k,v) in zip(keys,values):
211 self.userNS[k] = saferepr(v)
278 self.userNS[k] = saferepr(v)
212 self.didChangeValueForKey_('userNS')
279 self.didChangeValueForKey_('userNS')
213
280
214
281
215 def update_cell_prompt(self, result, blockID=None):
282 def update_cell_prompt(self, result, blockID=None):
283 print self.blockRanges
216 if(isinstance(result, Failure)):
284 if(isinstance(result, Failure)):
217 self.insert_text(self.input_prompt(),
285 prompt = self.input_prompt()
218 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
286
219 scrollToVisible=False
220 )
221 else:
287 else:
222 self.insert_text(self.input_prompt(number=result['number']),
288 prompt = self.input_prompt(number=result['number'])
223 textRange=NSMakeRange(self.blockRanges[blockID].location,0),
289
290 r = self.blockRanges[blockID].inputPromptRange
291 self.insert_text(prompt,
292 textRange=r,
224 scrollToVisible=False
293 scrollToVisible=False
225 )
294 )
226
295
227 return result
296 return result
228
297
229
298
230 def render_result(self, result):
299 def render_result(self, result):
231 blockID = result['blockID']
300 blockID = result['blockID']
232 inputRange = self.blockRanges[blockID]
301 inputRange = self.blockRanges[blockID].inputRange
233 del self.blockRanges[blockID]
302 del self.blockRanges[blockID]
234
303
235 #print inputRange,self.current_block_range()
304 #print inputRange,self.current_block_range()
236 self.insert_text('\n' +
305 self.insert_text('\n' +
237 self.output_prompt(number=result['number']) +
306 self.output_prompt(number=result['number']) +
238 result.get('display',{}).get('pprint','') +
307 result.get('display',{}).get('pprint','') +
239 '\n\n',
308 '\n\n',
240 textRange=NSMakeRange(inputRange.location+inputRange.length,
309 textRange=NSMakeRange(inputRange.location+inputRange.length,
241 0))
310 0))
242 return result
311 return result
243
312
244
313
245 def render_error(self, failure):
314 def render_error(self, failure):
315 print failure
316 blockID = failure.blockID
317 inputRange = self.blockRanges[blockID].inputRange
246 self.insert_text('\n' +
318 self.insert_text('\n' +
247 self.output_prompt() +
319 self.output_prompt() +
248 '\n' +
320 '\n' +
249 failure.getErrorMessage() +
321 failure.getErrorMessage() +
250 '\n\n')
322 '\n\n',
323 textRange=NSMakeRange(inputRange.location +
324 inputRange.length,
325 0))
251 self.start_new_block()
326 self.start_new_block()
252 return failure
327 return failure
253
328
254
329
255 def _start_cli_banner(self):
330 def _start_cli_banner(self):
256 """Print banner"""
331 """Print banner"""
257
332
258 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
333 banner = """IPython1 %s -- An enhanced Interactive Python.""" % \
259 IPython.__version__
334 IPython.__version__
260
335
261 self.insert_text(banner + '\n\n')
336 self.insert_text(banner + '\n\n')
262
337
263
338
264 def start_new_block(self):
339 def start_new_block(self):
265 """"""
340 """"""
266
341
267 self.currentBlockID = self.next_block_ID()
342 self.currentBlockID = self.next_block_ID()
343 self.blockRanges[self.currentBlockID] = self.new_cell_block()
344 self.insert_text(self.input_prompt(),
345 textRange=self.current_block_range().inputPromptRange)
268
346
269
347
270
348
271 def next_block_ID(self):
349 def next_block_ID(self):
272
350
273 return uuid.uuid4()
351 return uuid.uuid4()
274
352
353 def new_cell_block(self):
354 """A new CellBlock at the end of self.textView.textStorage()"""
355
356 return CellBlock(NSMakeRange(self.textView.textStorage().length()-1,
357 0), #len(self.input_prompt())),
358 NSMakeRange(self.textView.textStorage().length()-1,# + len(self.input_prompt()),
359 0))
360
361
275 def current_block_range(self):
362 def current_block_range(self):
276 return self.blockRanges.get(self.currentBlockID,
363 return self.blockRanges.get(self.currentBlockID,
277 NSMakeRange(self.textView.textStorage().length(),
364 self.new_cell_block())
278 0))
279
365
280 def current_block(self):
366 def current_block(self):
281 """The current block's text"""
367 """The current block's text"""
282
368
283 return self.text_for_range(self.current_block_range())
369 return self.text_for_range(self.current_block_range().inputRange)
284
370
285 def text_for_range(self, textRange):
371 def text_for_range(self, textRange):
286 """text_for_range"""
372 """text_for_range"""
287
373
288 ts = self.textView.textStorage()
374 ts = self.textView.textStorage()
289 return ts.string().substringWithRange_(textRange)
375 return ts.string().substringWithRange_(textRange)
290
376
291 def current_line(self):
377 def current_line(self):
292 block = self.text_for_range(self.current_block_range())
378 block = self.text_for_range(self.current_block_range().inputRange)
293 block = block.split('\n')
379 block = block.split('\n')
294 return block[-1]
380 return block[-1]
295
381
296
382
297 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
383 def insert_text(self, string=None, textRange=None, scrollToVisible=True):
298 """Insert text into textView at textRange, updating blockRanges
384 """Insert text into textView at textRange, updating blockRanges
299 as necessary
385 as necessary
300 """
386 """
301
302 if(textRange == None):
387 if(textRange == None):
303 #range for end of text
388 #range for end of text
304 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
389 textRange = NSMakeRange(self.textView.textStorage().length(), 0)
305
390
306 for r in self.blockRanges.itervalues():
307 intersection = NSIntersectionRange(r,textRange)
308 if(intersection.length == 0): #ranges don't intersect
309 if r.location >= textRange.location:
310 r.location += len(string)
311 else: #ranges intersect
312 if(r.location <= textRange.location):
313 assert(intersection.length == textRange.length)
314 r.length += textRange.length
315 else:
316 r.location += intersection.length
317
391
392 print textRange,string,self.textView.textStorage(),self.textView.textStorage().length()
318 self.textView.replaceCharactersInRange_withString_(
393 self.textView.replaceCharactersInRange_withString_(
319 textRange, string)
394 textRange, string)
320 self.textView.setSelectedRange_(
395
321 NSMakeRange(textRange.location+len(string), 0))
396 for r in self.blockRanges.itervalues():
397 r.update_ranges_for_insertion(string, textRange)
398
399 self.textView.setSelectedRange_(textRange)
322 if(scrollToVisible):
400 if(scrollToVisible):
323 self.textView.scrollRangeToVisible_(textRange)
401 self.textView.scrollRangeToVisible_(textRange)
324
402
325
403
326
404
327
328 def replace_current_block_with_string(self, textView, string):
405 def replace_current_block_with_string(self, textView, string):
329 textView.replaceCharactersInRange_withString_(
406 textView.replaceCharactersInRange_withString_(
330 self.current_block_range(),
407 self.current_block_range().inputRange,
331 string)
408 string)
332 self.current_block_range().length = len(string)
409 self.current_block_range().inputRange.length = len(string)
333 r = NSMakeRange(textView.textStorage().length(), 0)
410 r = NSMakeRange(textView.textStorage().length(), 0)
334 textView.scrollRangeToVisible_(r)
411 textView.scrollRangeToVisible_(r)
335 textView.setSelectedRange_(r)
412 textView.setSelectedRange_(r)
336
413
337
414
338 def current_indent_string(self):
415 def current_indent_string(self):
339 """returns string for indent or None if no indent"""
416 """returns string for indent or None if no indent"""
340
417
341 return self._indent_for_block(self.current_block())
418 return self._indent_for_block(self.current_block())
342
419
343
420
344 def _indent_for_block(self, block):
421 def _indent_for_block(self, block):
345 lines = block.split('\n')
422 lines = block.split('\n')
346 if(len(lines) > 1):
423 if(len(lines) > 1):
347 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
424 currentIndent = len(lines[-1]) - len(lines[-1].lstrip())
348 if(currentIndent == 0):
425 if(currentIndent == 0):
349 currentIndent = self.tabSpaces
426 currentIndent = self.tabSpaces
350
427
351 if(self.tabUsesSpaces):
428 if(self.tabUsesSpaces):
352 result = ' ' * currentIndent
429 result = ' ' * currentIndent
353 else:
430 else:
354 result = '\t' * (currentIndent/self.tabSpaces)
431 result = '\t' * (currentIndent/self.tabSpaces)
355 else:
432 else:
356 result = None
433 result = None
357
434
358 return result
435 return result
359
436
360
437
361 # NSTextView delegate methods...
438 # NSTextView delegate methods...
362 def textView_doCommandBySelector_(self, textView, selector):
439 def textView_doCommandBySelector_(self, textView, selector):
363 assert(textView == self.textView)
440 assert(textView == self.textView)
364 NSLog("textView_doCommandBySelector_: "+selector)
441 NSLog("textView_doCommandBySelector_: "+selector)
365
442
366
443
367 if(selector == 'insertNewline:'):
444 if(selector == 'insertNewline:'):
368 indent = self.current_indent_string()
445 indent = self.current_indent_string()
369 if(indent):
446 if(indent):
370 line = indent + self.current_line()
447 line = indent + self.current_line()
371 else:
448 else:
372 line = self.current_line()
449 line = self.current_line()
373
450
374 if(self.is_complete(self.current_block())):
451 if(self.is_complete(self.current_block())):
375 self.execute(self.current_block(),
452 self.execute(self.current_block(),
376 blockID=self.currentBlockID)
453 blockID=self.currentBlockID)
377 self.start_new_block()
454 self.start_new_block()
378
455
379 return True
456 return True
380
457
381 return False
458 return False
382
459
383 elif(selector == 'moveUp:'):
460 elif(selector == 'moveUp:'):
384 prevBlock = self.get_history_previous(self.current_block())
461 prevBlock = self.get_history_previous(self.current_block())
385 if(prevBlock != None):
462 if(prevBlock != None):
386 self.replace_current_block_with_string(textView, prevBlock)
463 self.replace_current_block_with_string(textView, prevBlock)
387 else:
464 else:
388 NSBeep()
465 NSBeep()
389 return True
466 return True
390
467
391 elif(selector == 'moveDown:'):
468 elif(selector == 'moveDown:'):
392 nextBlock = self.get_history_next()
469 nextBlock = self.get_history_next()
393 if(nextBlock != None):
470 if(nextBlock != None):
394 self.replace_current_block_with_string(textView, nextBlock)
471 self.replace_current_block_with_string(textView, nextBlock)
395 else:
472 else:
396 NSBeep()
473 NSBeep()
397 return True
474 return True
398
475
399 elif(selector == 'moveToBeginningOfParagraph:'):
476 elif(selector == 'moveToBeginningOfParagraph:'):
400 textView.setSelectedRange_(NSMakeRange(
477 textView.setSelectedRange_(NSMakeRange(
401 self.current_block_range().location,
478 self.current_block_range().inputRange.location,
402 0))
479 0))
403 return True
480 return True
404 elif(selector == 'moveToEndOfParagraph:'):
481 elif(selector == 'moveToEndOfParagraph:'):
405 textView.setSelectedRange_(NSMakeRange(
482 textView.setSelectedRange_(NSMakeRange(
406 self.current_block_range().location + \
483 self.current_block_range().inputRange.location + \
407 self.current_block_range().length, 0))
484 self.current_block_range().inputRange.length, 0))
408 return True
485 return True
409 elif(selector == 'deleteToEndOfParagraph:'):
486 elif(selector == 'deleteToEndOfParagraph:'):
410 if(textView.selectedRange().location <= \
487 if(textView.selectedRange().location <= \
411 self.current_block_range().location):
488 self.current_block_range().location):
412 # Intersect the selected range with the current line range
489 raise NotImplemented()
413 if(self.current_block_range().length < 0):
414 self.blockRanges[self.currentBlockID].length = 0
415
416 r = NSIntersectionRange(textView.rangesForUserTextChange()[0],
417 self.current_block_range())
418
419 if(r.length > 0): #no intersection
420 textView.setSelectedRange_(r)
421
490
422 return False # don't actually handle the delete
491 return False # don't actually handle the delete
423
492
424 elif(selector == 'insertTab:'):
493 elif(selector == 'insertTab:'):
425 if(len(self.current_line().strip()) == 0): #only white space
494 if(len(self.current_line().strip()) == 0): #only white space
426 return False
495 return False
427 else:
496 else:
428 self.textView.complete_(self)
497 self.textView.complete_(self)
429 return True
498 return True
430
499
431 elif(selector == 'deleteBackward:'):
500 elif(selector == 'deleteBackward:'):
432 #if we're at the beginning of the current block, ignore
501 #if we're at the beginning of the current block, ignore
433 if(textView.selectedRange().location == \
502 if(textView.selectedRange().location == \
434 self.current_block_range().location):
503 self.current_block_range().inputRange.location):
435 return True
504 return True
436 else:
505 else:
437 self.current_block_range().length-=1
506 for r in self.blockRanges.itervalues():
507 deleteRange = textView.selectedRange
508 if(deleteRange.length == 0):
509 deleteRange.location -= 1
510 deleteRange.length = 1
511 r.update_ranges_for_deletion(deleteRange)
438 return False
512 return False
439 return False
513 return False
440
514
441
515
442 def textView_shouldChangeTextInRanges_replacementStrings_(self,
516 def textView_shouldChangeTextInRanges_replacementStrings_(self,
443 textView, ranges, replacementStrings):
517 textView, ranges, replacementStrings):
444 """
518 """
445 Delegate method for NSTextView.
519 Delegate method for NSTextView.
446
520
447 Refuse change text in ranges not at end, but make those changes at
521 Refuse change text in ranges not at end, but make those changes at
448 end.
522 end.
449 """
523 """
450
524
451 assert(len(ranges) == len(replacementStrings))
525 assert(len(ranges) == len(replacementStrings))
452 allow = True
526 allow = True
453 for r,s in zip(ranges, replacementStrings):
527 for r,s in zip(ranges, replacementStrings):
454 r = r.rangeValue()
528 r = r.rangeValue()
455 if(textView.textStorage().length() > 0 and
529 if(textView.textStorage().length() > 0 and
456 r.location < self.current_block_range().location):
530 r.location < self.current_block_range().inputRange.location):
457 self.insert_text(s)
531 self.insert_text(s, textRange=r)
458 allow = False
532 allow = False
459
533
460
461 self.blockRanges.setdefault(self.currentBlockID,
462 self.current_block_range()).length +=\
463 len(s)
464
465 return allow
534 return allow
466
535
467 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
536 def textView_completions_forPartialWordRange_indexOfSelectedItem_(self,
468 textView, words, charRange, index):
537 textView, words, charRange, index):
469 try:
538 try:
470 ts = textView.textStorage()
539 ts = textView.textStorage()
471 token = ts.string().substringWithRange_(charRange)
540 token = ts.string().substringWithRange_(charRange)
472 completions = blockingCallFromThread(self.complete, token)
541 completions = blockingCallFromThread(self.complete, token)
473 except:
542 except:
474 completions = objc.nil
543 completions = objc.nil
475 NSBeep()
544 NSBeep()
476
545
477 return (completions,0)
546 return (completions,0)
478
547
479
548
@@ -1,403 +1,400 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
2 # -*- test-case-name: IPython.frontend.tests.test_frontendbase -*-
3 """
3 """
4 frontendbase provides an interface and base class for GUI frontends for
4 frontendbase provides an interface and base class for GUI frontends for
5 IPython.kernel/IPython.kernel.core.
5 IPython.kernel/IPython.kernel.core.
6
6
7 Frontend implementations will likely want to subclass FrontEndBase.
7 Frontend implementations will likely want to subclass FrontEndBase.
8
8
9 Author: Barry Wark
9 Author: Barry Wark
10 """
10 """
11 __docformat__ = "restructuredtext en"
11 __docformat__ = "restructuredtext en"
12
12
13 #-------------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
14 # Copyright (C) 2008 The IPython Development Team
14 # Copyright (C) 2008 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-------------------------------------------------------------------------------
18 #-------------------------------------------------------------------------------
19
19
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-------------------------------------------------------------------------------
22 #-------------------------------------------------------------------------------
23 import string
23 import string
24 import uuid
24 import uuid
25 import _ast
25 import _ast
26
26
27 try:
27 try:
28 from zope.interface import Interface, Attribute, implements, classProvides
28 from zope.interface import Interface, Attribute, implements, classProvides
29 except ImportError:
29 except ImportError:
30 #zope.interface is not available
30 #zope.interface is not available
31 Interface = object
31 Interface = object
32 def Attribute(name, doc): pass
32 def Attribute(name, doc): pass
33 def implements(interface): pass
33 def implements(interface): pass
34 def classProvides(interface): pass
34 def classProvides(interface): pass
35
35
36 from IPython.kernel.core.history import FrontEndHistory
36 from IPython.kernel.core.history import FrontEndHistory
37 from IPython.kernel.core.util import Bunch
37 from IPython.kernel.core.util import Bunch
38 from IPython.kernel.engineservice import IEngineCore
38 from IPython.kernel.engineservice import IEngineCore
39
39
40
40
41 ##############################################################################
41 ##############################################################################
42 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
42 # TEMPORARY!!! fake configuration, while we decide whether to use tconfig or
43 # not
43 # not
44
44
45 rc = Bunch()
45 rc = Bunch()
46 rc.prompt_in1 = r'In [$number]: '
46 rc.prompt_in1 = r'In [$number]: '
47 rc.prompt_in2 = r'...'
47 rc.prompt_in2 = r'...'
48 rc.prompt_out = r'Out [$number]: '
48 rc.prompt_out = r'Out [$number]: '
49
49
50 ##############################################################################
50 ##############################################################################
51
51
52 class IFrontEndFactory(Interface):
52 class IFrontEndFactory(Interface):
53 """Factory interface for frontends."""
53 """Factory interface for frontends."""
54
54
55 def __call__(engine=None, history=None):
55 def __call__(engine=None, history=None):
56 """
56 """
57 Parameters:
57 Parameters:
58 interpreter : IPython.kernel.engineservice.IEngineCore
58 interpreter : IPython.kernel.engineservice.IEngineCore
59 """
59 """
60
60
61 pass
61 pass
62
62
63
63
64
64
65 class IFrontEnd(Interface):
65 class IFrontEnd(Interface):
66 """Interface for frontends. All methods return t.i.d.Deferred"""
66 """Interface for frontends. All methods return t.i.d.Deferred"""
67
67
68 Attribute("input_prompt_template", "string.Template instance\
68 Attribute("input_prompt_template", "string.Template instance\
69 substituteable with execute result.")
69 substituteable with execute result.")
70 Attribute("output_prompt_template", "string.Template instance\
70 Attribute("output_prompt_template", "string.Template instance\
71 substituteable with execute result.")
71 substituteable with execute result.")
72 Attribute("continuation_prompt_template", "string.Template instance\
72 Attribute("continuation_prompt_template", "string.Template instance\
73 substituteable with execute result.")
73 substituteable with execute result.")
74
74
75 def update_cell_prompt(result, blockID=None):
75 def update_cell_prompt(result, blockID=None):
76 """Subclass may override to update the input prompt for a block.
76 """Subclass may override to update the input prompt for a block.
77 Since this method will be called as a
77 Since this method will be called as a
78 twisted.internet.defer.Deferred's callback/errback,
78 twisted.internet.defer.Deferred's callback/errback,
79 implementations should return result when finished.
79 implementations should return result when finished.
80
80
81 Result is a result dict in case of success, and a
81 Result is a result dict in case of success, and a
82 twisted.python.util.failure.Failure in case of an error
82 twisted.python.util.failure.Failure in case of an error
83 """
83 """
84
84
85 pass
85 pass
86
86
87
87
88 def render_result(result):
88 def render_result(result):
89 """Render the result of an execute call. Implementors may choose the
89 """Render the result of an execute call. Implementors may choose the
90 method of rendering.
90 method of rendering.
91 For example, a notebook-style frontend might render a Chaco plot
91 For example, a notebook-style frontend might render a Chaco plot
92 inline.
92 inline.
93
93
94 Parameters:
94 Parameters:
95 result : dict (result of IEngineBase.execute )
95 result : dict (result of IEngineBase.execute )
96 blockID = result['blockID']
96 blockID = result['blockID']
97
97
98 Result:
98 Result:
99 Output of frontend rendering
99 Output of frontend rendering
100 """
100 """
101
101
102 pass
102 pass
103
103
104 def render_error(failure):
104 def render_error(failure):
105 """Subclasses must override to render the failure. Since this method
105 """Subclasses must override to render the failure. Since this method
106 will be called as a twisted.internet.defer.Deferred's callback,
106 will be called as a twisted.internet.defer.Deferred's callback,
107 implementations should return result when finished.
107 implementations should return result when finished.
108
108
109 blockID = failure.blockID
109 blockID = failure.blockID
110 """
110 """
111
111
112 pass
112 pass
113
113
114
114
115 def input_prompt(number=''):
115 def input_prompt(number=''):
116 """Returns the input prompt by subsituting into
116 """Returns the input prompt by subsituting into
117 self.input_prompt_template
117 self.input_prompt_template
118 """
118 """
119 pass
119 pass
120
120
121 def output_prompt(number=''):
121 def output_prompt(number=''):
122 """Returns the output prompt by subsituting into
122 """Returns the output prompt by subsituting into
123 self.output_prompt_template
123 self.output_prompt_template
124 """
124 """
125
125
126 pass
126 pass
127
127
128 def continuation_prompt():
128 def continuation_prompt():
129 """Returns the continuation prompt by subsituting into
129 """Returns the continuation prompt by subsituting into
130 self.continuation_prompt_template
130 self.continuation_prompt_template
131 """
131 """
132
132
133 pass
133 pass
134
134
135 def is_complete(block):
135 def is_complete(block):
136 """Returns True if block is complete, False otherwise."""
136 """Returns True if block is complete, False otherwise."""
137
137
138 pass
138 pass
139
139
140 def compile_ast(block):
140 def compile_ast(block):
141 """Compiles block to an _ast.AST"""
141 """Compiles block to an _ast.AST"""
142
142
143 pass
143 pass
144
144
145
145
146 def get_history_previous(currentBlock):
146 def get_history_previous(currentBlock):
147 """Returns the block previous in the history. Saves currentBlock if
147 """Returns the block previous in the history. Saves currentBlock if
148 the history_cursor is currently at the end of the input history"""
148 the history_cursor is currently at the end of the input history"""
149 pass
149 pass
150
150
151 def get_history_next():
151 def get_history_next():
152 """Returns the next block in the history."""
152 """Returns the next block in the history."""
153
153
154 pass
154 pass
155
155
156
156
157 class FrontEndBase(object):
157 class FrontEndBase(object):
158 """
158 """
159 FrontEndBase manages the state tasks for a CLI frontend:
159 FrontEndBase manages the state tasks for a CLI frontend:
160 - Input and output history management
160 - Input and output history management
161 - Input/continuation and output prompt generation
161 - Input/continuation and output prompt generation
162
162
163 Some issues (due to possibly unavailable engine):
163 Some issues (due to possibly unavailable engine):
164 - How do we get the current cell number for the engine?
164 - How do we get the current cell number for the engine?
165 - How do we handle completions?
165 - How do we handle completions?
166 """
166 """
167
167
168 history_cursor = 0
168 history_cursor = 0
169
169
170 current_indent_level = 0
170 current_indent_level = 0
171
171
172
172
173 input_prompt_template = string.Template(rc.prompt_in1)
173 input_prompt_template = string.Template(rc.prompt_in1)
174 output_prompt_template = string.Template(rc.prompt_out)
174 output_prompt_template = string.Template(rc.prompt_out)
175 continuation_prompt_template = string.Template(rc.prompt_in2)
175 continuation_prompt_template = string.Template(rc.prompt_in2)
176
176
177 def __init__(self, shell=None, history=None):
177 def __init__(self, shell=None, history=None):
178 self.shell = shell
178 self.shell = shell
179 if history is None:
179 if history is None:
180 self.history = FrontEndHistory(input_cache=[''])
180 self.history = FrontEndHistory(input_cache=[''])
181 else:
181 else:
182 self.history = history
182 self.history = history
183
183
184
184
185 def input_prompt(self, number=''):
185 def input_prompt(self, number=''):
186 """Returns the current input prompt
186 """Returns the current input prompt
187
187
188 It would be great to use ipython1.core.prompts.Prompt1 here
188 It would be great to use ipython1.core.prompts.Prompt1 here
189 """
189 """
190 return self.input_prompt_template.safe_substitute({'number':number})
190 return self.input_prompt_template.safe_substitute({'number':number})
191
191
192
192
193 def continuation_prompt(self):
193 def continuation_prompt(self):
194 """Returns the current continuation prompt"""
194 """Returns the current continuation prompt"""
195
195
196 return self.continuation_prompt_template.safe_substitute()
196 return self.continuation_prompt_template.safe_substitute()
197
197
198 def output_prompt(self, number=''):
198 def output_prompt(self, number=''):
199 """Returns the output prompt for result"""
199 """Returns the output prompt for result"""
200
200
201 return self.output_prompt_template.safe_substitute({'number':number})
201 return self.output_prompt_template.safe_substitute({'number':number})
202
202
203
203
204 def is_complete(self, block):
204 def is_complete(self, block):
205 """Determine if block is complete.
205 """Determine if block is complete.
206
206
207 Parameters
207 Parameters
208 block : string
208 block : string
209
209
210 Result
210 Result
211 True if block can be sent to the engine without compile errors.
211 True if block can be sent to the engine without compile errors.
212 False otherwise.
212 False otherwise.
213 """
213 """
214
214
215 try:
215 try:
216 ast = self.compile_ast(block)
216 ast = self.compile_ast(block)
217 except:
217 except:
218 return False
218 return False
219
219
220 lines = block.split('\n')
220 lines = block.split('\n')
221 return (len(lines)==1 or str(lines[-1])=='')
221 return (len(lines)==1 or str(lines[-1])=='')
222
222
223
223
224 def compile_ast(self, block):
224 def compile_ast(self, block):
225 """Compile block to an AST
225 """Compile block to an AST
226
226
227 Parameters:
227 Parameters:
228 block : str
228 block : str
229
229
230 Result:
230 Result:
231 AST
231 AST
232
232
233 Throws:
233 Throws:
234 Exception if block cannot be compiled
234 Exception if block cannot be compiled
235 """
235 """
236
236
237 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
237 return compile(block, "<string>", "exec", _ast.PyCF_ONLY_AST)
238
238
239
239
240 def execute(self, block, blockID=None):
240 def execute(self, block, blockID=None):
241 """Execute the block and return the result.
241 """Execute the block and return the result.
242
242
243 Parameters:
243 Parameters:
244 block : {str, AST}
244 block : {str, AST}
245 blockID : any
245 blockID : any
246 Caller may provide an ID to identify this block.
246 Caller may provide an ID to identify this block.
247 result['blockID'] := blockID
247 result['blockID'] := blockID
248
248
249 Result:
249 Result:
250 Deferred result of self.interpreter.execute
250 Deferred result of self.interpreter.execute
251 """
251 """
252
252
253 if(not self.is_complete(block)):
253 if(not self.is_complete(block)):
254 raise Exception("Block is not compilable")
254 raise Exception("Block is not compilable")
255
255
256 if(blockID == None):
256 if(blockID == None):
257 blockID = uuid.uuid4() #random UUID
257 blockID = uuid.uuid4() #random UUID
258
258
259 try:
259 try:
260 result = self.shell.execute(block)
260 result = self.shell.execute(block)
261 except Exception,e:
261 except Exception,e:
262 e = self._add_block_id_for_failure(e, blockID=blockID)
262 e = self._add_block_id_for_failure(e, blockID=blockID)
263 e = self.update_cell_prompt(e, blockID=blockID)
263 e = self.update_cell_prompt(e, blockID=blockID)
264 e = self.render_error(e)
264 e = self.render_error(e)
265 else:
265 else:
266 result = self._add_block_id_for_result(result, blockID=blockID)
266 result = self._add_block_id_for_result(result, blockID=blockID)
267 result = self.update_cell_prompt(result, blockID=blockID)
267 result = self.update_cell_prompt(result, blockID=blockID)
268 result = self.render_result(result)
268 result = self.render_result(result)
269
269
270 return result
270 return result
271
271
272
272
273 def _add_block_id_for_result(self, result, blockID):
273 def _add_block_id_for_result(self, result, blockID):
274 """Add the blockID to result or failure. Unfortunatley, we have to
274 """Add the blockID to result or failure. Unfortunatley, we have to
275 treat failures differently than result dicts.
275 treat failures differently than result dicts.
276 """
276 """
277
277
278 result['blockID'] = blockID
278 result['blockID'] = blockID
279
279
280 return result
280 return result
281
281
282 def _add_block_id_for_failure(self, failure, blockID):
282 def _add_block_id_for_failure(self, failure, blockID):
283 """_add_block_id_for_failure"""
283 """_add_block_id_for_failure"""
284
285 failure.blockID = blockID
284 failure.blockID = blockID
286 return failure
285 return failure
287
286
288
287
289 def _add_history(self, result, block=None):
288 def _add_history(self, result, block=None):
290 """Add block to the history"""
289 """Add block to the history"""
291
290
292 assert(block != None)
291 assert(block != None)
293 self.history.add_items([block])
292 self.history.add_items([block])
294 self.history_cursor += 1
293 self.history_cursor += 1
295
294
296 return result
295 return result
297
296
298
297
299 def get_history_previous(self, currentBlock):
298 def get_history_previous(self, currentBlock):
300 """ Returns previous history string and decrement history cursor.
299 """ Returns previous history string and decrement history cursor.
301 """
300 """
302 command = self.history.get_history_item(self.history_cursor - 1)
301 command = self.history.get_history_item(self.history_cursor - 1)
303
302
304 if command is not None:
303 if command is not None:
305 if(self.history_cursor == len(self.history.input_cache)):
304 if(self.history_cursor == len(self.history.input_cache)):
306 self.history.input_cache[self.history_cursor] = currentBlock
305 self.history.input_cache[self.history_cursor] = currentBlock
307 self.history_cursor -= 1
306 self.history_cursor -= 1
308 return command
307 return command
309
308
310
309
311 def get_history_next(self):
310 def get_history_next(self):
312 """ Returns next history string and increment history cursor.
311 """ Returns next history string and increment history cursor.
313 """
312 """
314 command = self.history.get_history_item(self.history_cursor+1)
313 command = self.history.get_history_item(self.history_cursor+1)
315
314
316 if command is not None:
315 if command is not None:
317 self.history_cursor += 1
316 self.history_cursor += 1
318 return command
317 return command
319
318
320 ###
319 ###
321 # Subclasses probably want to override these methods...
320 # Subclasses probably want to override these methods...
322 ###
321 ###
323
322
324 def update_cell_prompt(self, result, blockID=None):
323 def update_cell_prompt(self, result, blockID=None):
325 """Subclass may override to update the input prompt for a block.
324 """Subclass may override to update the input prompt for a block.
326 Since this method will be called as a
325 Since this method will be called as a
327 twisted.internet.defer.Deferred's callback, implementations should
326 twisted.internet.defer.Deferred's callback, implementations should
328 return result when finished.
327 return result when finished.
329 """
328 """
330
329
331 return result
330 return result
332
331
333
332
334 def render_result(self, result):
333 def render_result(self, result):
335 """Subclasses must override to render result. Since this method will
334 """Subclasses must override to render result. Since this method will
336 be called as a twisted.internet.defer.Deferred's callback,
335 be called as a twisted.internet.defer.Deferred's callback,
337 implementations should return result when finished.
336 implementations should return result when finished.
338 """
337 """
339
338
340 return result
339 return result
341
340
342
341
343 def render_error(self, failure):
342 def render_error(self, failure):
344 """Subclasses must override to render the failure. Since this method
343 """Subclasses must override to render the failure. Since this method
345 will be called as a twisted.internet.defer.Deferred's callback,
344 will be called as a twisted.internet.defer.Deferred's callback,
346 implementations should return result when finished.
345 implementations should return result when finished.
347 """
346 """
348
347
349 return failure
348 return failure
350
349
351
350
352
351
353 class AsyncFrontEndBase(FrontEndBase):
352 class AsyncFrontEndBase(FrontEndBase):
354 """
353 """
355 Overrides FrontEndBase to wrap execute in a deferred result.
354 Overrides FrontEndBase to wrap execute in a deferred result.
356 All callbacks are made as callbacks on the deferred result.
355 All callbacks are made as callbacks on the deferred result.
357 """
356 """
358
357
359 implements(IFrontEnd)
358 implements(IFrontEnd)
360 classProvides(IFrontEndFactory)
359 classProvides(IFrontEndFactory)
361
360
362 def __init__(self, engine=None, history=None):
361 def __init__(self, engine=None, history=None):
363 assert(engine==None or IEngineCore.providedBy(engine))
362 assert(engine==None or IEngineCore.providedBy(engine))
364 self.engine = IEngineCore(engine)
363 self.engine = IEngineCore(engine)
365 if history is None:
364 if history is None:
366 self.history = FrontEndHistory(input_cache=[''])
365 self.history = FrontEndHistory(input_cache=[''])
367 else:
366 else:
368 self.history = history
367 self.history = history
369
368
370
369
371 def execute(self, block, blockID=None):
370 def execute(self, block, blockID=None):
372 """Execute the block and return the deferred result.
371 """Execute the block and return the deferred result.
373
372
374 Parameters:
373 Parameters:
375 block : {str, AST}
374 block : {str, AST}
376 blockID : any
375 blockID : any
377 Caller may provide an ID to identify this block.
376 Caller may provide an ID to identify this block.
378 result['blockID'] := blockID
377 result['blockID'] := blockID
379
378
380 Result:
379 Result:
381 Deferred result of self.interpreter.execute
380 Deferred result of self.interpreter.execute
382 """
381 """
383
382
384 if(not self.is_complete(block)):
383 if(not self.is_complete(block)):
385 from twisted.python.failure import Failure
384 from twisted.python.failure import Failure
386 return Failure(Exception("Block is not compilable"))
385 return Failure(Exception("Block is not compilable"))
387
386
388 if(blockID == None):
387 if(blockID == None):
389 blockID = uuid.uuid4() #random UUID
388 blockID = uuid.uuid4() #random UUID
390
389
391 d = self.engine.execute(block)
390 d = self.engine.execute(block)
392 d.addCallback(self._add_history, block=block)
391 d.addCallback(self._add_history, block=block)
393 d.addCallbacks(self._add_block_id_for_result,
392 d.addCallback(self._add_block_id_for_result, blockID)
394 errback=self._add_block_id_for_failure,
393 d.addErrback(self._add_block_id_for_failure, blockID)
395 callbackArgs=(blockID,),
396 errbackArgs=(blockID,))
397 d.addBoth(self.update_cell_prompt, blockID=blockID)
394 d.addBoth(self.update_cell_prompt, blockID=blockID)
398 d.addCallbacks(self.render_result,
395 d.addCallbacks(self.render_result,
399 errback=self.render_error)
396 errback=self.render_error)
400
397
401 return d
398 return d
402
399
403
400
@@ -1,876 +1,901 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 # -*- test-case-name: IPython.kernel.tests.test_engineservice -*-
2 # -*- test-case-name: IPython.kernel.tests.test_engineservice -*-
3
3
4 """A Twisted Service Representation of the IPython core.
4 """A Twisted Service Representation of the IPython core.
5
5
6 The IPython Core exposed to the network is called the Engine. Its
6 The IPython Core exposed to the network is called the Engine. Its
7 representation in Twisted in the EngineService. Interfaces and adapters
7 representation in Twisted in the EngineService. Interfaces and adapters
8 are used to abstract out the details of the actual network protocol used.
8 are used to abstract out the details of the actual network protocol used.
9 The EngineService is an Engine that knows nothing about the actual protocol
9 The EngineService is an Engine that knows nothing about the actual protocol
10 used.
10 used.
11
11
12 The EngineService is exposed with various network protocols in modules like:
12 The EngineService is exposed with various network protocols in modules like:
13
13
14 enginepb.py
14 enginepb.py
15 enginevanilla.py
15 enginevanilla.py
16
16
17 As of 12/12/06 the classes in this module have been simplified greatly. It was
17 As of 12/12/06 the classes in this module have been simplified greatly. It was
18 felt that we had over-engineered things. To improve the maintainability of the
18 felt that we had over-engineered things. To improve the maintainability of the
19 code we have taken out the ICompleteEngine interface and the completeEngine
19 code we have taken out the ICompleteEngine interface and the completeEngine
20 method that automatically added methods to engines.
20 method that automatically added methods to engines.
21
21
22 """
22 """
23
23
24 __docformat__ = "restructuredtext en"
24 __docformat__ = "restructuredtext en"
25
25
26 #-------------------------------------------------------------------------------
26 #-------------------------------------------------------------------------------
27 # Copyright (C) 2008 The IPython Development Team
27 # Copyright (C) 2008 The IPython Development Team
28 #
28 #
29 # Distributed under the terms of the BSD License. The full license is in
29 # Distributed under the terms of the BSD License. The full license is in
30 # the file COPYING, distributed as part of this software.
30 # the file COPYING, distributed as part of this software.
31 #-------------------------------------------------------------------------------
31 #-------------------------------------------------------------------------------
32
32
33 #-------------------------------------------------------------------------------
33 #-------------------------------------------------------------------------------
34 # Imports
34 # Imports
35 #-------------------------------------------------------------------------------
35 #-------------------------------------------------------------------------------
36
36
37 import os, sys, copy
37 import os, sys, copy
38 import cPickle as pickle
38 import cPickle as pickle
39 from new import instancemethod
39 from new import instancemethod
40
40
41 from twisted.application import service
41 from twisted.application import service
42 from twisted.internet import defer, reactor
42 from twisted.internet import defer, reactor
43 from twisted.python import log, failure, components
43 from twisted.python import log, failure, components
44 import zope.interface as zi
44 import zope.interface as zi
45
45
46 from IPython.kernel.core.interpreter import Interpreter
46 from IPython.kernel.core.interpreter import Interpreter
47 from IPython.kernel import newserialized, error, util
47 from IPython.kernel import newserialized, error, util
48 from IPython.kernel.util import printer
48 from IPython.kernel.util import printer
49 from IPython.kernel.twistedutil import gatherBoth, DeferredList
49 from IPython.kernel.twistedutil import gatherBoth, DeferredList
50 from IPython.kernel import codeutil
50 from IPython.kernel import codeutil
51
51
52
52
53 #-------------------------------------------------------------------------------
53 #-------------------------------------------------------------------------------
54 # Interface specification for the Engine
54 # Interface specification for the Engine
55 #-------------------------------------------------------------------------------
55 #-------------------------------------------------------------------------------
56
56
57 class IEngineCore(zi.Interface):
57 class IEngineCore(zi.Interface):
58 """The minimal required interface for the IPython Engine.
58 """The minimal required interface for the IPython Engine.
59
59
60 This interface provides a formal specification of the IPython core.
60 This interface provides a formal specification of the IPython core.
61 All these methods should return deferreds regardless of what side of a
61 All these methods should return deferreds regardless of what side of a
62 network connection they are on.
62 network connection they are on.
63
63
64 In general, this class simply wraps a shell class and wraps its return
64 In general, this class simply wraps a shell class and wraps its return
65 values as Deferred objects. If the underlying shell class method raises
65 values as Deferred objects. If the underlying shell class method raises
66 an exception, this class should convert it to a twisted.failure.Failure
66 an exception, this class should convert it to a twisted.failure.Failure
67 that will be propagated along the Deferred's errback chain.
67 that will be propagated along the Deferred's errback chain.
68
68
69 In addition, Failures are aggressive. By this, we mean that if a method
69 In addition, Failures are aggressive. By this, we mean that if a method
70 is performing multiple actions (like pulling multiple object) if any
70 is performing multiple actions (like pulling multiple object) if any
71 single one fails, the entire method will fail with that Failure. It is
71 single one fails, the entire method will fail with that Failure. It is
72 all or nothing.
72 all or nothing.
73 """
73 """
74
74
75 id = zi.interface.Attribute("the id of the Engine object")
75 id = zi.interface.Attribute("the id of the Engine object")
76 properties = zi.interface.Attribute("A dict of properties of the Engine")
76 properties = zi.interface.Attribute("A dict of properties of the Engine")
77
77
78 def execute(lines):
78 def execute(lines):
79 """Execute lines of Python code.
79 """Execute lines of Python code.
80
80
81 Returns a dictionary with keys (id, number, stdin, stdout, stderr)
81 Returns a dictionary with keys (id, number, stdin, stdout, stderr)
82 upon success.
82 upon success.
83
83
84 Returns a failure object if the execution of lines raises an exception.
84 Returns a failure object if the execution of lines raises an exception.
85 """
85 """
86
86
87 def push(namespace):
87 def push(namespace):
88 """Push dict namespace into the user's namespace.
88 """Push dict namespace into the user's namespace.
89
89
90 Returns a deferred to None or a failure.
90 Returns a deferred to None or a failure.
91 """
91 """
92
92
93 def pull(keys):
93 def pull(keys):
94 """Pulls values out of the user's namespace by keys.
94 """Pulls values out of the user's namespace by keys.
95
95
96 Returns a deferred to a tuple objects or a single object.
96 Returns a deferred to a tuple objects or a single object.
97
97
98 Raises NameError if any one of objects doess not exist.
98 Raises NameError if any one of objects doess not exist.
99 """
99 """
100
100
101 def push_function(namespace):
101 def push_function(namespace):
102 """Push a dict of key, function pairs into the user's namespace.
102 """Push a dict of key, function pairs into the user's namespace.
103
103
104 Returns a deferred to None or a failure."""
104 Returns a deferred to None or a failure."""
105
105
106 def pull_function(keys):
106 def pull_function(keys):
107 """Pulls functions out of the user's namespace by keys.
107 """Pulls functions out of the user's namespace by keys.
108
108
109 Returns a deferred to a tuple of functions or a single function.
109 Returns a deferred to a tuple of functions or a single function.
110
110
111 Raises NameError if any one of the functions does not exist.
111 Raises NameError if any one of the functions does not exist.
112 """
112 """
113
113
114 def get_result(i=None):
114 def get_result(i=None):
115 """Get the stdin/stdout/stderr of command i.
115 """Get the stdin/stdout/stderr of command i.
116
116
117 Returns a deferred to a dict with keys
117 Returns a deferred to a dict with keys
118 (id, number, stdin, stdout, stderr).
118 (id, number, stdin, stdout, stderr).
119
119
120 Raises IndexError if command i does not exist.
120 Raises IndexError if command i does not exist.
121 Raises TypeError if i in not an int.
121 Raises TypeError if i in not an int.
122 """
122 """
123
123
124 def reset():
124 def reset():
125 """Reset the shell.
125 """Reset the shell.
126
126
127 This clears the users namespace. Won't cause modules to be
127 This clears the users namespace. Won't cause modules to be
128 reloaded. Should also re-initialize certain variables like id.
128 reloaded. Should also re-initialize certain variables like id.
129 """
129 """
130
130
131 def kill():
131 def kill():
132 """Kill the engine by stopping the reactor."""
132 """Kill the engine by stopping the reactor."""
133
133
134 def keys():
134 def keys():
135 """Return the top level variables in the users namspace.
135 """Return the top level variables in the users namspace.
136
136
137 Returns a deferred to a dict."""
137 Returns a deferred to a dict."""
138
138
139
139
140 class IEngineSerialized(zi.Interface):
140 class IEngineSerialized(zi.Interface):
141 """Push/Pull methods that take Serialized objects.
141 """Push/Pull methods that take Serialized objects.
142
142
143 All methods should return deferreds.
143 All methods should return deferreds.
144 """
144 """
145
145
146 def push_serialized(namespace):
146 def push_serialized(namespace):
147 """Push a dict of keys and Serialized objects into the user's namespace."""
147 """Push a dict of keys and Serialized objects into the user's namespace."""
148
148
149 def pull_serialized(keys):
149 def pull_serialized(keys):
150 """Pull objects by key from the user's namespace as Serialized.
150 """Pull objects by key from the user's namespace as Serialized.
151
151
152 Returns a list of or one Serialized.
152 Returns a list of or one Serialized.
153
153
154 Raises NameError is any one of the objects does not exist.
154 Raises NameError is any one of the objects does not exist.
155 """
155 """
156
156
157
157
158 class IEngineProperties(zi.Interface):
158 class IEngineProperties(zi.Interface):
159 """Methods for access to the properties object of an Engine"""
159 """Methods for access to the properties object of an Engine"""
160
160
161 properties = zi.Attribute("A StrictDict object, containing the properties")
161 properties = zi.Attribute("A StrictDict object, containing the properties")
162
162
163 def set_properties(properties):
163 def set_properties(properties):
164 """set properties by key and value"""
164 """set properties by key and value"""
165
165
166 def get_properties(keys=None):
166 def get_properties(keys=None):
167 """get a list of properties by `keys`, if no keys specified, get all"""
167 """get a list of properties by `keys`, if no keys specified, get all"""
168
168
169 def del_properties(keys):
169 def del_properties(keys):
170 """delete properties by `keys`"""
170 """delete properties by `keys`"""
171
171
172 def has_properties(keys):
172 def has_properties(keys):
173 """get a list of bool values for whether `properties` has `keys`"""
173 """get a list of bool values for whether `properties` has `keys`"""
174
174
175 def clear_properties():
175 def clear_properties():
176 """clear the properties dict"""
176 """clear the properties dict"""
177
177
178 class IEngineBase(IEngineCore, IEngineSerialized, IEngineProperties):
178 class IEngineBase(IEngineCore, IEngineSerialized, IEngineProperties):
179 """The basic engine interface that EngineService will implement.
179 """The basic engine interface that EngineService will implement.
180
180
181 This exists so it is easy to specify adapters that adapt to and from the
181 This exists so it is easy to specify adapters that adapt to and from the
182 API that the basic EngineService implements.
182 API that the basic EngineService implements.
183 """
183 """
184 pass
184 pass
185
185
186 class IEngineQueued(IEngineBase):
186 class IEngineQueued(IEngineBase):
187 """Interface for adding a queue to an IEngineBase.
187 """Interface for adding a queue to an IEngineBase.
188
188
189 This interface extends the IEngineBase interface to add methods for managing
189 This interface extends the IEngineBase interface to add methods for managing
190 the engine's queue. The implicit details of this interface are that the
190 the engine's queue. The implicit details of this interface are that the
191 execution of all methods declared in IEngineBase should appropriately be
191 execution of all methods declared in IEngineBase should appropriately be
192 put through a queue before execution.
192 put through a queue before execution.
193
193
194 All methods should return deferreds.
194 All methods should return deferreds.
195 """
195 """
196
196
197 def clear_queue():
197 def clear_queue():
198 """Clear the queue."""
198 """Clear the queue."""
199
199
200 def queue_status():
200 def queue_status():
201 """Get the queued and pending commands in the queue."""
201 """Get the queued and pending commands in the queue."""
202
202
203 def register_failure_observer(obs):
203 def register_failure_observer(obs):
204 """Register an observer of pending Failures.
204 """Register an observer of pending Failures.
205
205
206 The observer must implement IFailureObserver.
206 The observer must implement IFailureObserver.
207 """
207 """
208
208
209 def unregister_failure_observer(obs):
209 def unregister_failure_observer(obs):
210 """Unregister an observer of pending Failures."""
210 """Unregister an observer of pending Failures."""
211
211
212
212
213 class IEngineThreaded(zi.Interface):
213 class IEngineThreaded(zi.Interface):
214 """A place holder for threaded commands.
214 """A place holder for threaded commands.
215
215
216 All methods should return deferreds.
216 All methods should return deferreds.
217 """
217 """
218 pass
218 pass
219
219
220
220
221 #-------------------------------------------------------------------------------
221 #-------------------------------------------------------------------------------
222 # Functions and classes to implement the EngineService
222 # Functions and classes to implement the EngineService
223 #-------------------------------------------------------------------------------
223 #-------------------------------------------------------------------------------
224
224
225
225
226 class StrictDict(dict):
226 class StrictDict(dict):
227 """This is a strict copying dictionary for use as the interface to the
227 """This is a strict copying dictionary for use as the interface to the
228 properties of an Engine.
228 properties of an Engine.
229 :IMPORTANT:
229 :IMPORTANT:
230 This object copies the values you set to it, and returns copies to you
230 This object copies the values you set to it, and returns copies to you
231 when you request them. The only way to change properties os explicitly
231 when you request them. The only way to change properties os explicitly
232 through the setitem and getitem of the dictionary interface.
232 through the setitem and getitem of the dictionary interface.
233 Example:
233 Example:
234 >>> e = kernel.get_engine(id)
234 >>> e = kernel.get_engine(id)
235 >>> L = someList
235 >>> L = someList
236 >>> e.properties['L'] = L
236 >>> e.properties['L'] = L
237 >>> L == e.properties['L']
237 >>> L == e.properties['L']
238 ... True
238 ... True
239 >>> L.append(something Else)
239 >>> L.append(something Else)
240 >>> L == e.properties['L']
240 >>> L == e.properties['L']
241 ... False
241 ... False
242
242
243 getitem copies, so calls to methods of objects do not affect the
243 getitem copies, so calls to methods of objects do not affect the
244 properties, as in the following example:
244 properties, as in the following example:
245 >>> e.properties[1] = range(2)
245 >>> e.properties[1] = range(2)
246 >>> print e.properties[1]
246 >>> print e.properties[1]
247 ... [0, 1]
247 ... [0, 1]
248 >>> e.properties[1].append(2)
248 >>> e.properties[1].append(2)
249 >>> print e.properties[1]
249 >>> print e.properties[1]
250 ... [0, 1]
250 ... [0, 1]
251
251
252 """
252 """
253 def __init__(self, *args, **kwargs):
253 def __init__(self, *args, **kwargs):
254 dict.__init__(self, *args, **kwargs)
254 dict.__init__(self, *args, **kwargs)
255 self.modified = True
255 self.modified = True
256
256
257 def __getitem__(self, key):
257 def __getitem__(self, key):
258 return copy.deepcopy(dict.__getitem__(self, key))
258 return copy.deepcopy(dict.__getitem__(self, key))
259
259
260 def __setitem__(self, key, value):
260 def __setitem__(self, key, value):
261 # check if this entry is valid for transport around the network
261 # check if this entry is valid for transport around the network
262 # and copying
262 # and copying
263 try:
263 try:
264 pickle.dumps(key, 2)
264 pickle.dumps(key, 2)
265 pickle.dumps(value, 2)
265 pickle.dumps(value, 2)
266 newvalue = copy.deepcopy(value)
266 newvalue = copy.deepcopy(value)
267 except:
267 except:
268 raise error.InvalidProperty(value)
268 raise error.InvalidProperty(value)
269 dict.__setitem__(self, key, newvalue)
269 dict.__setitem__(self, key, newvalue)
270 self.modified = True
270 self.modified = True
271
271
272 def __delitem__(self, key):
272 def __delitem__(self, key):
273 dict.__delitem__(self, key)
273 dict.__delitem__(self, key)
274 self.modified = True
274 self.modified = True
275
275
276 def update(self, dikt):
276 def update(self, dikt):
277 for k,v in dikt.iteritems():
277 for k,v in dikt.iteritems():
278 self[k] = v
278 self[k] = v
279
279
280 def pop(self, key):
280 def pop(self, key):
281 self.modified = True
281 self.modified = True
282 return dict.pop(self, key)
282 return dict.pop(self, key)
283
283
284 def popitem(self):
284 def popitem(self):
285 self.modified = True
285 self.modified = True
286 return dict.popitem(self)
286 return dict.popitem(self)
287
287
288 def clear(self):
288 def clear(self):
289 self.modified = True
289 self.modified = True
290 dict.clear(self)
290 dict.clear(self)
291
291
292 def subDict(self, *keys):
292 def subDict(self, *keys):
293 d = {}
293 d = {}
294 for key in keys:
294 for key in keys:
295 d[key] = self[key]
295 d[key] = self[key]
296 return d
296 return d
297
297
298
298
299
299
300 class EngineAPI(object):
300 class EngineAPI(object):
301 """This is the object through which the user can edit the `properties`
301 """This is the object through which the user can edit the `properties`
302 attribute of an Engine.
302 attribute of an Engine.
303 The Engine Properties object copies all object in and out of itself.
303 The Engine Properties object copies all object in and out of itself.
304 See the EngineProperties object for details.
304 See the EngineProperties object for details.
305 """
305 """
306 _fix=False
306 _fix=False
307 def __init__(self, id):
307 def __init__(self, id):
308 self.id = id
308 self.id = id
309 self.properties = StrictDict()
309 self.properties = StrictDict()
310 self._fix=True
310 self._fix=True
311
311
312 def __setattr__(self, k,v):
312 def __setattr__(self, k,v):
313 if self._fix:
313 if self._fix:
314 raise error.KernelError("I am protected!")
314 raise error.KernelError("I am protected!")
315 else:
315 else:
316 object.__setattr__(self, k, v)
316 object.__setattr__(self, k, v)
317
317
318 def __delattr__(self, key):
318 def __delattr__(self, key):
319 raise error.KernelError("I am protected!")
319 raise error.KernelError("I am protected!")
320
320
321
321
322 _apiDict = {}
322 _apiDict = {}
323
323
324 def get_engine(id):
324 def get_engine(id):
325 """Get the Engine API object, whcih currently just provides the properties
325 """Get the Engine API object, whcih currently just provides the properties
326 object, by ID"""
326 object, by ID"""
327 global _apiDict
327 global _apiDict
328 if not _apiDict.get(id):
328 if not _apiDict.get(id):
329 _apiDict[id] = EngineAPI(id)
329 _apiDict[id] = EngineAPI(id)
330 return _apiDict[id]
330 return _apiDict[id]
331
331
332 def drop_engine(id):
332 def drop_engine(id):
333 """remove an engine"""
333 """remove an engine"""
334 global _apiDict
334 global _apiDict
335 if _apiDict.has_key(id):
335 if _apiDict.has_key(id):
336 del _apiDict[id]
336 del _apiDict[id]
337
337
338 class EngineService(object, service.Service):
338 class EngineService(object, service.Service):
339 """Adapt a IPython shell into a IEngine implementing Twisted Service."""
339 """Adapt a IPython shell into a IEngine implementing Twisted Service."""
340
340
341 zi.implements(IEngineBase)
341 zi.implements(IEngineBase)
342 name = 'EngineService'
342 name = 'EngineService'
343
343
344 def __init__(self, shellClass=Interpreter, mpi=None):
344 def __init__(self, shellClass=Interpreter, mpi=None):
345 """Create an EngineService.
345 """Create an EngineService.
346
346
347 shellClass: something that implements IInterpreter or core1
347 shellClass: something that implements IInterpreter or core1
348 mpi: an mpi module that has rank and size attributes
348 mpi: an mpi module that has rank and size attributes
349 """
349 """
350 self.shellClass = shellClass
350 self.shellClass = shellClass
351 self.shell = self.shellClass()
351 self.shell = self.shellClass()
352 self.mpi = mpi
352 self.mpi = mpi
353 self.id = None
353 self.id = None
354 self.properties = get_engine(self.id).properties
354 self.properties = get_engine(self.id).properties
355 if self.mpi is not None:
355 if self.mpi is not None:
356 log.msg("MPI started with rank = %i and size = %i" %
356 log.msg("MPI started with rank = %i and size = %i" %
357 (self.mpi.rank, self.mpi.size))
357 (self.mpi.rank, self.mpi.size))
358 self.id = self.mpi.rank
358 self.id = self.mpi.rank
359 self._seedNamespace()
359 self._seedNamespace()
360
360
361 # Make id a property so that the shell can get the updated id
361 # Make id a property so that the shell can get the updated id
362
362
363 def _setID(self, id):
363 def _setID(self, id):
364 self._id = id
364 self._id = id
365 self.properties = get_engine(id).properties
365 self.properties = get_engine(id).properties
366 self.shell.push({'id': id})
366 self.shell.push({'id': id})
367
367
368 def _getID(self):
368 def _getID(self):
369 return self._id
369 return self._id
370
370
371 id = property(_getID, _setID)
371 id = property(_getID, _setID)
372
372
373 def _seedNamespace(self):
373 def _seedNamespace(self):
374 self.shell.push({'mpi': self.mpi, 'id' : self.id})
374 self.shell.push({'mpi': self.mpi, 'id' : self.id})
375
375
376 def executeAndRaise(self, msg, callable, *args, **kwargs):
376 def executeAndRaise(self, msg, callable, *args, **kwargs):
377 """Call a method of self.shell and wrap any exception."""
377 """Call a method of self.shell and wrap any exception."""
378 d = defer.Deferred()
378 d = defer.Deferred()
379 try:
379 try:
380 result = callable(*args, **kwargs)
380 result = callable(*args, **kwargs)
381 except:
381 except:
382 # This gives the following:
382 # This gives the following:
383 # et=exception class
383 # et=exception class
384 # ev=exception class instance
384 # ev=exception class instance
385 # tb=traceback object
385 # tb=traceback object
386 et,ev,tb = sys.exc_info()
386 et,ev,tb = sys.exc_info()
387 # This call adds attributes to the exception value
387 # This call adds attributes to the exception value
388 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
388 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
389 # Add another attribute
389 # Add another attribute
390 ev._ipython_engine_info = msg
390 ev._ipython_engine_info = msg
391 f = failure.Failure(ev,et,None)
391 f = failure.Failure(ev,et,None)
392 d.errback(f)
392 d.errback(f)
393 else:
393 else:
394 d.callback(result)
394 d.callback(result)
395
395
396 return d
396 return d
397
397
398
398 # The IEngine methods. See the interface for documentation.
399 # The IEngine methods. See the interface for documentation.
399
400
400 def execute(self, lines):
401 def execute(self, lines):
401 msg = {'engineid':self.id,
402 msg = {'engineid':self.id,
402 'method':'execute',
403 'method':'execute',
403 'args':[lines]}
404 'args':[lines]}
404 d = self.executeAndRaise(msg, self.shell.execute, lines)
405 d = self.executeAndRaise(msg, self.shell.execute, lines)
405 d.addCallback(self.addIDToResult)
406 d.addCallback(self.addIDToResult)
406 return d
407 return d
407
408
408 def addIDToResult(self, result):
409 def addIDToResult(self, result):
409 result['id'] = self.id
410 result['id'] = self.id
410 return result
411 return result
411
412
412 def push(self, namespace):
413 def push(self, namespace):
413 msg = {'engineid':self.id,
414 msg = {'engineid':self.id,
414 'method':'push',
415 'method':'push',
415 'args':[repr(namespace.keys())]}
416 'args':[repr(namespace.keys())]}
416 d = self.executeAndRaise(msg, self.shell.push, namespace)
417 d = self.executeAndRaise(msg, self.shell.push, namespace)
417 return d
418 return d
418
419
419 def pull(self, keys):
420 def pull(self, keys):
420 msg = {'engineid':self.id,
421 msg = {'engineid':self.id,
421 'method':'pull',
422 'method':'pull',
422 'args':[repr(keys)]}
423 'args':[repr(keys)]}
423 d = self.executeAndRaise(msg, self.shell.pull, keys)
424 d = self.executeAndRaise(msg, self.shell.pull, keys)
424 return d
425 return d
425
426
426 def push_function(self, namespace):
427 def push_function(self, namespace):
427 msg = {'engineid':self.id,
428 msg = {'engineid':self.id,
428 'method':'push_function',
429 'method':'push_function',
429 'args':[repr(namespace.keys())]}
430 'args':[repr(namespace.keys())]}
430 d = self.executeAndRaise(msg, self.shell.push_function, namespace)
431 d = self.executeAndRaise(msg, self.shell.push_function, namespace)
431 return d
432 return d
432
433
433 def pull_function(self, keys):
434 def pull_function(self, keys):
434 msg = {'engineid':self.id,
435 msg = {'engineid':self.id,
435 'method':'pull_function',
436 'method':'pull_function',
436 'args':[repr(keys)]}
437 'args':[repr(keys)]}
437 d = self.executeAndRaise(msg, self.shell.pull_function, keys)
438 d = self.executeAndRaise(msg, self.shell.pull_function, keys)
438 return d
439 return d
439
440
440 def get_result(self, i=None):
441 def get_result(self, i=None):
441 msg = {'engineid':self.id,
442 msg = {'engineid':self.id,
442 'method':'get_result',
443 'method':'get_result',
443 'args':[repr(i)]}
444 'args':[repr(i)]}
444 d = self.executeAndRaise(msg, self.shell.getCommand, i)
445 d = self.executeAndRaise(msg, self.shell.getCommand, i)
445 d.addCallback(self.addIDToResult)
446 d.addCallback(self.addIDToResult)
446 return d
447 return d
447
448
448 def reset(self):
449 def reset(self):
449 msg = {'engineid':self.id,
450 msg = {'engineid':self.id,
450 'method':'reset',
451 'method':'reset',
451 'args':[]}
452 'args':[]}
452 del self.shell
453 del self.shell
453 self.shell = self.shellClass()
454 self.shell = self.shellClass()
454 self.properties.clear()
455 self.properties.clear()
455 d = self.executeAndRaise(msg, self._seedNamespace)
456 d = self.executeAndRaise(msg, self._seedNamespace)
456 return d
457 return d
457
458
458 def kill(self):
459 def kill(self):
459 drop_engine(self.id)
460 drop_engine(self.id)
460 try:
461 try:
461 reactor.stop()
462 reactor.stop()
462 except RuntimeError:
463 except RuntimeError:
463 log.msg('The reactor was not running apparently.')
464 log.msg('The reactor was not running apparently.')
464 return defer.fail()
465 return defer.fail()
465 else:
466 else:
466 return defer.succeed(None)
467 return defer.succeed(None)
467
468
468 def keys(self):
469 def keys(self):
469 """Return a list of variables names in the users top level namespace.
470 """Return a list of variables names in the users top level namespace.
470
471
471 This used to return a dict of all the keys/repr(values) in the
472 This used to return a dict of all the keys/repr(values) in the
472 user's namespace. This was too much info for the ControllerService
473 user's namespace. This was too much info for the ControllerService
473 to handle so it is now just a list of keys.
474 to handle so it is now just a list of keys.
474 """
475 """
475
476
476 remotes = []
477 remotes = []
477 for k in self.shell.user_ns.iterkeys():
478 for k in self.shell.user_ns.iterkeys():
478 if k not in ['__name__', '_ih', '_oh', '__builtins__',
479 if k not in ['__name__', '_ih', '_oh', '__builtins__',
479 'In', 'Out', '_', '__', '___', '__IP', 'input', 'raw_input']:
480 'In', 'Out', '_', '__', '___', '__IP', 'input', 'raw_input']:
480 remotes.append(k)
481 remotes.append(k)
481 return defer.succeed(remotes)
482 return defer.succeed(remotes)
482
483
483 def set_properties(self, properties):
484 def set_properties(self, properties):
484 msg = {'engineid':self.id,
485 msg = {'engineid':self.id,
485 'method':'set_properties',
486 'method':'set_properties',
486 'args':[repr(properties.keys())]}
487 'args':[repr(properties.keys())]}
487 return self.executeAndRaise(msg, self.properties.update, properties)
488 return self.executeAndRaise(msg, self.properties.update, properties)
488
489
489 def get_properties(self, keys=None):
490 def get_properties(self, keys=None):
490 msg = {'engineid':self.id,
491 msg = {'engineid':self.id,
491 'method':'get_properties',
492 'method':'get_properties',
492 'args':[repr(keys)]}
493 'args':[repr(keys)]}
493 if keys is None:
494 if keys is None:
494 keys = self.properties.keys()
495 keys = self.properties.keys()
495 return self.executeAndRaise(msg, self.properties.subDict, *keys)
496 return self.executeAndRaise(msg, self.properties.subDict, *keys)
496
497
497 def _doDel(self, keys):
498 def _doDel(self, keys):
498 for key in keys:
499 for key in keys:
499 del self.properties[key]
500 del self.properties[key]
500
501
501 def del_properties(self, keys):
502 def del_properties(self, keys):
502 msg = {'engineid':self.id,
503 msg = {'engineid':self.id,
503 'method':'del_properties',
504 'method':'del_properties',
504 'args':[repr(keys)]}
505 'args':[repr(keys)]}
505 return self.executeAndRaise(msg, self._doDel, keys)
506 return self.executeAndRaise(msg, self._doDel, keys)
506
507
507 def _doHas(self, keys):
508 def _doHas(self, keys):
508 return [self.properties.has_key(key) for key in keys]
509 return [self.properties.has_key(key) for key in keys]
509
510
510 def has_properties(self, keys):
511 def has_properties(self, keys):
511 msg = {'engineid':self.id,
512 msg = {'engineid':self.id,
512 'method':'has_properties',
513 'method':'has_properties',
513 'args':[repr(keys)]}
514 'args':[repr(keys)]}
514 return self.executeAndRaise(msg, self._doHas, keys)
515 return self.executeAndRaise(msg, self._doHas, keys)
515
516
516 def clear_properties(self):
517 def clear_properties(self):
517 msg = {'engineid':self.id,
518 msg = {'engineid':self.id,
518 'method':'clear_properties',
519 'method':'clear_properties',
519 'args':[]}
520 'args':[]}
520 return self.executeAndRaise(msg, self.properties.clear)
521 return self.executeAndRaise(msg, self.properties.clear)
521
522
522 def push_serialized(self, sNamespace):
523 def push_serialized(self, sNamespace):
523 msg = {'engineid':self.id,
524 msg = {'engineid':self.id,
524 'method':'push_serialized',
525 'method':'push_serialized',
525 'args':[repr(sNamespace.keys())]}
526 'args':[repr(sNamespace.keys())]}
526 ns = {}
527 ns = {}
527 for k,v in sNamespace.iteritems():
528 for k,v in sNamespace.iteritems():
528 try:
529 try:
529 unserialized = newserialized.IUnSerialized(v)
530 unserialized = newserialized.IUnSerialized(v)
530 ns[k] = unserialized.getObject()
531 ns[k] = unserialized.getObject()
531 except:
532 except:
532 return defer.fail()
533 return defer.fail()
533 return self.executeAndRaise(msg, self.shell.push, ns)
534 return self.executeAndRaise(msg, self.shell.push, ns)
534
535
535 def pull_serialized(self, keys):
536 def pull_serialized(self, keys):
536 msg = {'engineid':self.id,
537 msg = {'engineid':self.id,
537 'method':'pull_serialized',
538 'method':'pull_serialized',
538 'args':[repr(keys)]}
539 'args':[repr(keys)]}
539 if isinstance(keys, str):
540 if isinstance(keys, str):
540 keys = [keys]
541 keys = [keys]
541 if len(keys)==1:
542 if len(keys)==1:
542 d = self.executeAndRaise(msg, self.shell.pull, keys)
543 d = self.executeAndRaise(msg, self.shell.pull, keys)
543 d.addCallback(newserialized.serialize)
544 d.addCallback(newserialized.serialize)
544 return d
545 return d
545 elif len(keys)>1:
546 elif len(keys)>1:
546 d = self.executeAndRaise(msg, self.shell.pull, keys)
547 d = self.executeAndRaise(msg, self.shell.pull, keys)
547 @d.addCallback
548 @d.addCallback
548 def packThemUp(values):
549 def packThemUp(values):
549 serials = []
550 serials = []
550 for v in values:
551 for v in values:
551 try:
552 try:
552 serials.append(newserialized.serialize(v))
553 serials.append(newserialized.serialize(v))
553 except:
554 except:
554 return defer.fail(failure.Failure())
555 return defer.fail(failure.Failure())
555 return serials
556 return serials
556 return packThemUp
557 return packThemUp
557
558
558
559
559 def queue(methodToQueue):
560 def queue(methodToQueue):
560 def queuedMethod(this, *args, **kwargs):
561 def queuedMethod(this, *args, **kwargs):
561 name = methodToQueue.__name__
562 name = methodToQueue.__name__
562 return this.submitCommand(Command(name, *args, **kwargs))
563 return this.submitCommand(Command(name, *args, **kwargs))
563 return queuedMethod
564 return queuedMethod
564
565
565 class QueuedEngine(object):
566 class QueuedEngine(object):
566 """Adapt an IEngineBase to an IEngineQueued by wrapping it.
567 """Adapt an IEngineBase to an IEngineQueued by wrapping it.
567
568
568 The resulting object will implement IEngineQueued which extends
569 The resulting object will implement IEngineQueued which extends
569 IEngineCore which extends (IEngineBase, IEngineSerialized).
570 IEngineCore which extends (IEngineBase, IEngineSerialized).
570
571
571 This seems like the best way of handling it, but I am not sure. The
572 This seems like the best way of handling it, but I am not sure. The
572 other option is to have the various base interfaces be used like
573 other option is to have the various base interfaces be used like
573 mix-in intefaces. The problem I have with this is adpatation is
574 mix-in intefaces. The problem I have with this is adpatation is
574 more difficult and complicated because there can be can multiple
575 more difficult and complicated because there can be can multiple
575 original and final Interfaces.
576 original and final Interfaces.
576 """
577 """
577
578
578 zi.implements(IEngineQueued)
579 zi.implements(IEngineQueued)
579
580
580 def __init__(self, engine):
581 def __init__(self, engine):
581 """Create a QueuedEngine object from an engine
582 """Create a QueuedEngine object from an engine
582
583
583 engine: An implementor of IEngineCore and IEngineSerialized
584 engine: An implementor of IEngineCore and IEngineSerialized
584 keepUpToDate: whether to update the remote status when the
585 keepUpToDate: whether to update the remote status when the
585 queue is empty. Defaults to False.
586 queue is empty. Defaults to False.
586 """
587 """
587
588
588 # This is the right way to do these tests rather than
589 # This is the right way to do these tests rather than
589 # IEngineCore in list(zi.providedBy(engine)) which will only
590 # IEngineCore in list(zi.providedBy(engine)) which will only
590 # picks of the interfaces that are directly declared by engine.
591 # picks of the interfaces that are directly declared by engine.
591 assert IEngineBase.providedBy(engine), \
592 assert IEngineBase.providedBy(engine), \
592 "engine passed to QueuedEngine doesn't provide IEngineBase"
593 "engine passed to QueuedEngine doesn't provide IEngineBase"
593
594
594 self.engine = engine
595 self.engine = engine
595 self.id = engine.id
596 self.id = engine.id
596 self.queued = []
597 self.queued = []
597 self.history = {}
598 self.history = {}
598 self.engineStatus = {}
599 self.engineStatus = {}
599 self.currentCommand = None
600 self.currentCommand = None
600 self.failureObservers = []
601 self.failureObservers = []
601
602
602 def _get_properties(self):
603 def _get_properties(self):
603 return self.engine.properties
604 return self.engine.properties
604
605
605 properties = property(_get_properties, lambda self, _: None)
606 properties = property(_get_properties, lambda self, _: None)
606 # Queue management methods. You should not call these directly
607 # Queue management methods. You should not call these directly
607
608
608 def submitCommand(self, cmd):
609 def submitCommand(self, cmd):
609 """Submit command to queue."""
610 """Submit command to queue."""
610
611
611 d = defer.Deferred()
612 d = defer.Deferred()
612 cmd.setDeferred(d)
613 cmd.setDeferred(d)
613 if self.currentCommand is not None:
614 if self.currentCommand is not None:
614 if self.currentCommand.finished:
615 if self.currentCommand.finished:
615 # log.msg("Running command immediately: %r" % cmd)
616 # log.msg("Running command immediately: %r" % cmd)
616 self.currentCommand = cmd
617 self.currentCommand = cmd
617 self.runCurrentCommand()
618 self.runCurrentCommand()
618 else: # command is still running
619 else: # command is still running
619 # log.msg("Command is running: %r" % self.currentCommand)
620 # log.msg("Command is running: %r" % self.currentCommand)
620 # log.msg("Queueing: %r" % cmd)
621 # log.msg("Queueing: %r" % cmd)
621 self.queued.append(cmd)
622 self.queued.append(cmd)
622 else:
623 else:
623 # log.msg("No current commands, running: %r" % cmd)
624 # log.msg("No current commands, running: %r" % cmd)
624 self.currentCommand = cmd
625 self.currentCommand = cmd
625 self.runCurrentCommand()
626 self.runCurrentCommand()
626 return d
627 return d
627
628
628 def runCurrentCommand(self):
629 def runCurrentCommand(self):
629 """Run current command."""
630 """Run current command."""
630
631
631 cmd = self.currentCommand
632 cmd = self.currentCommand
632 f = getattr(self.engine, cmd.remoteMethod, None)
633 f = getattr(self.engine, cmd.remoteMethod, None)
633 if f:
634 if f:
634 d = f(*cmd.args, **cmd.kwargs)
635 d = f(*cmd.args, **cmd.kwargs)
635 if cmd.remoteMethod is 'execute':
636 if cmd.remoteMethod is 'execute':
636 d.addCallback(self.saveResult)
637 d.addCallback(self.saveResult)
637 d.addCallback(self.finishCommand)
638 d.addCallback(self.finishCommand)
638 d.addErrback(self.abortCommand)
639 d.addErrback(self.abortCommand)
639 else:
640 else:
640 return defer.fail(AttributeError(cmd.remoteMethod))
641 return defer.fail(AttributeError(cmd.remoteMethod))
641
642
642 def _flushQueue(self):
643 def _flushQueue(self):
643 """Pop next command in queue and run it."""
644 """Pop next command in queue and run it."""
644
645
645 if len(self.queued) > 0:
646 if len(self.queued) > 0:
646 self.currentCommand = self.queued.pop(0)
647 self.currentCommand = self.queued.pop(0)
647 self.runCurrentCommand()
648 self.runCurrentCommand()
648
649
649 def saveResult(self, result):
650 def saveResult(self, result):
650 """Put the result in the history."""
651 """Put the result in the history."""
651 self.history[result['number']] = result
652 self.history[result['number']] = result
652 return result
653 return result
653
654
654 def finishCommand(self, result):
655 def finishCommand(self, result):
655 """Finish currrent command."""
656 """Finish currrent command."""
656
657
657 # The order of these commands is absolutely critical.
658 # The order of these commands is absolutely critical.
658 self.currentCommand.handleResult(result)
659 self.currentCommand.handleResult(result)
659 self.currentCommand.finished = True
660 self.currentCommand.finished = True
660 self._flushQueue()
661 self._flushQueue()
661 return result
662 return result
662
663
663 def abortCommand(self, reason):
664 def abortCommand(self, reason):
664 """Abort current command.
665 """Abort current command.
665
666
666 This eats the Failure but first passes it onto the Deferred that the
667 This eats the Failure but first passes it onto the Deferred that the
667 user has.
668 user has.
668
669
669 It also clear out the queue so subsequence commands don't run.
670 It also clear out the queue so subsequence commands don't run.
670 """
671 """
671
672
672 # The order of these 3 commands is absolutely critical. The currentCommand
673 # The order of these 3 commands is absolutely critical. The currentCommand
673 # must first be marked as finished BEFORE the queue is cleared and before
674 # must first be marked as finished BEFORE the queue is cleared and before
674 # the current command is sent the failure.
675 # the current command is sent the failure.
675 # Also, the queue must be cleared BEFORE the current command is sent the Failure
676 # Also, the queue must be cleared BEFORE the current command is sent the Failure
676 # otherwise the errback chain could trigger new commands to be added to the
677 # otherwise the errback chain could trigger new commands to be added to the
677 # queue before we clear it. We should clear ONLY the commands that were in
678 # queue before we clear it. We should clear ONLY the commands that were in
678 # the queue when the error occured.
679 # the queue when the error occured.
679 self.currentCommand.finished = True
680 self.currentCommand.finished = True
680 s = "%r %r %r" % (self.currentCommand.remoteMethod, self.currentCommand.args, self.currentCommand.kwargs)
681 s = "%r %r %r" % (self.currentCommand.remoteMethod, self.currentCommand.args, self.currentCommand.kwargs)
681 self.clear_queue(msg=s)
682 self.clear_queue(msg=s)
682 self.currentCommand.handleError(reason)
683 self.currentCommand.handleError(reason)
683
684
684 return None
685 return None
685
686
686 #---------------------------------------------------------------------------
687 #---------------------------------------------------------------------------
687 # IEngineCore methods
688 # IEngineCore methods
688 #---------------------------------------------------------------------------
689 #---------------------------------------------------------------------------
689
690
690 @queue
691 @queue
691 def execute(self, lines):
692 def execute(self, lines):
692 pass
693 pass
693
694
694 @queue
695 @queue
695 def push(self, namespace):
696 def push(self, namespace):
696 pass
697 pass
697
698
698 @queue
699 @queue
699 def pull(self, keys):
700 def pull(self, keys):
700 pass
701 pass
701
702
702 @queue
703 @queue
703 def push_function(self, namespace):
704 def push_function(self, namespace):
704 pass
705 pass
705
706
706 @queue
707 @queue
707 def pull_function(self, keys):
708 def pull_function(self, keys):
708 pass
709 pass
709
710
710 def get_result(self, i=None):
711 def get_result(self, i=None):
711 if i is None:
712 if i is None:
712 i = max(self.history.keys()+[None])
713 i = max(self.history.keys()+[None])
713
714
714 cmd = self.history.get(i, None)
715 cmd = self.history.get(i, None)
715 # Uncomment this line to disable chaching of results
716 # Uncomment this line to disable chaching of results
716 #cmd = None
717 #cmd = None
717 if cmd is None:
718 if cmd is None:
718 return self.submitCommand(Command('get_result', i))
719 return self.submitCommand(Command('get_result', i))
719 else:
720 else:
720 return defer.succeed(cmd)
721 return defer.succeed(cmd)
721
722
722 def reset(self):
723 def reset(self):
723 self.clear_queue()
724 self.clear_queue()
724 self.history = {} # reset the cache - I am not sure we should do this
725 self.history = {} # reset the cache - I am not sure we should do this
725 return self.submitCommand(Command('reset'))
726 return self.submitCommand(Command('reset'))
726
727
727 def kill(self):
728 def kill(self):
728 self.clear_queue()
729 self.clear_queue()
729 return self.submitCommand(Command('kill'))
730 return self.submitCommand(Command('kill'))
730
731
731 @queue
732 @queue
732 def keys(self):
733 def keys(self):
733 pass
734 pass
734
735
735 #---------------------------------------------------------------------------
736 #---------------------------------------------------------------------------
736 # IEngineSerialized methods
737 # IEngineSerialized methods
737 #---------------------------------------------------------------------------
738 #---------------------------------------------------------------------------
738
739
739 @queue
740 @queue
740 def push_serialized(self, namespace):
741 def push_serialized(self, namespace):
741 pass
742 pass
742
743
743 @queue
744 @queue
744 def pull_serialized(self, keys):
745 def pull_serialized(self, keys):
745 pass
746 pass
746
747
747 #---------------------------------------------------------------------------
748 #---------------------------------------------------------------------------
748 # IEngineProperties methods
749 # IEngineProperties methods
749 #---------------------------------------------------------------------------
750 #---------------------------------------------------------------------------
750
751
751 @queue
752 @queue
752 def set_properties(self, namespace):
753 def set_properties(self, namespace):
753 pass
754 pass
754
755
755 @queue
756 @queue
756 def get_properties(self, keys=None):
757 def get_properties(self, keys=None):
757 pass
758 pass
758
759
759 @queue
760 @queue
760 def del_properties(self, keys):
761 def del_properties(self, keys):
761 pass
762 pass
762
763
763 @queue
764 @queue
764 def has_properties(self, keys):
765 def has_properties(self, keys):
765 pass
766 pass
766
767
767 @queue
768 @queue
768 def clear_properties(self):
769 def clear_properties(self):
769 pass
770 pass
770
771
771 #---------------------------------------------------------------------------
772 #---------------------------------------------------------------------------
772 # IQueuedEngine methods
773 # IQueuedEngine methods
773 #---------------------------------------------------------------------------
774 #---------------------------------------------------------------------------
774
775
775 def clear_queue(self, msg=''):
776 def clear_queue(self, msg=''):
776 """Clear the queue, but doesn't cancel the currently running commmand."""
777 """Clear the queue, but doesn't cancel the currently running commmand."""
777
778
778 for cmd in self.queued:
779 for cmd in self.queued:
779 cmd.deferred.errback(failure.Failure(error.QueueCleared(msg)))
780 cmd.deferred.errback(failure.Failure(error.QueueCleared(msg)))
780 self.queued = []
781 self.queued = []
781 return defer.succeed(None)
782 return defer.succeed(None)
782
783
783 def queue_status(self):
784 def queue_status(self):
784 if self.currentCommand is not None:
785 if self.currentCommand is not None:
785 if self.currentCommand.finished:
786 if self.currentCommand.finished:
786 pending = repr(None)
787 pending = repr(None)
787 else:
788 else:
788 pending = repr(self.currentCommand)
789 pending = repr(self.currentCommand)
789 else:
790 else:
790 pending = repr(None)
791 pending = repr(None)
791 dikt = {'queue':map(repr,self.queued), 'pending':pending}
792 dikt = {'queue':map(repr,self.queued), 'pending':pending}
792 return defer.succeed(dikt)
793 return defer.succeed(dikt)
793
794
794 def register_failure_observer(self, obs):
795 def register_failure_observer(self, obs):
795 self.failureObservers.append(obs)
796 self.failureObservers.append(obs)
796
797
797 def unregister_failure_observer(self, obs):
798 def unregister_failure_observer(self, obs):
798 self.failureObservers.remove(obs)
799 self.failureObservers.remove(obs)
799
800
800
801
801 # Now register QueuedEngine as an adpater class that makes an IEngineBase into a
802 # Now register QueuedEngine as an adpater class that makes an IEngineBase into a
802 # IEngineQueued.
803 # IEngineQueued.
803 components.registerAdapter(QueuedEngine, IEngineBase, IEngineQueued)
804 components.registerAdapter(QueuedEngine, IEngineBase, IEngineQueued)
804
805
805
806
806 class Command(object):
807 class Command(object):
807 """A command object that encapslates queued commands.
808 """A command object that encapslates queued commands.
808
809
809 This class basically keeps track of a command that has been queued
810 This class basically keeps track of a command that has been queued
810 in a QueuedEngine. It manages the deferreds and hold the method to be called
811 in a QueuedEngine. It manages the deferreds and hold the method to be called
811 and the arguments to that method.
812 and the arguments to that method.
812 """
813 """
813
814
814
815
815 def __init__(self, remoteMethod, *args, **kwargs):
816 def __init__(self, remoteMethod, *args, **kwargs):
816 """Build a new Command object."""
817 """Build a new Command object."""
817
818
818 self.remoteMethod = remoteMethod
819 self.remoteMethod = remoteMethod
819 self.args = args
820 self.args = args
820 self.kwargs = kwargs
821 self.kwargs = kwargs
821 self.finished = False
822 self.finished = False
822
823
823 def setDeferred(self, d):
824 def setDeferred(self, d):
824 """Sets the deferred attribute of the Command."""
825 """Sets the deferred attribute of the Command."""
825
826
826 self.deferred = d
827 self.deferred = d
827
828
828 def __repr__(self):
829 def __repr__(self):
829 if not self.args:
830 if not self.args:
830 args = ''
831 args = ''
831 else:
832 else:
832 args = str(self.args)[1:-2] #cut off (...,)
833 args = str(self.args)[1:-2] #cut off (...,)
833 for k,v in self.kwargs.iteritems():
834 for k,v in self.kwargs.iteritems():
834 if args:
835 if args:
835 args += ', '
836 args += ', '
836 args += '%s=%r' %(k,v)
837 args += '%s=%r' %(k,v)
837 return "%s(%s)" %(self.remoteMethod, args)
838 return "%s(%s)" %(self.remoteMethod, args)
838
839
839 def handleResult(self, result):
840 def handleResult(self, result):
840 """When the result is ready, relay it to self.deferred."""
841 """When the result is ready, relay it to self.deferred."""
841
842
842 self.deferred.callback(result)
843 self.deferred.callback(result)
843
844
844 def handleError(self, reason):
845 def handleError(self, reason):
845 """When an error has occured, relay it to self.deferred."""
846 """When an error has occured, relay it to self.deferred."""
846
847
847 self.deferred.errback(reason)
848 self.deferred.errback(reason)
848
849
849 class ThreadedEngineService(EngineService):
850 class ThreadedEngineService(EngineService):
850 """An EngineService subclass that defers execute commands to a separate
851 """An EngineService subclass that defers execute commands to a separate
851 thread.
852 thread.
852
853
853 ThreadedEngineService uses twisted.internet.threads.deferToThread to
854 ThreadedEngineService uses twisted.internet.threads.deferToThread to
854 defer execute requests to a separate thread. GUI frontends may want to
855 defer execute requests to a separate thread. GUI frontends may want to
855 use ThreadedEngineService as the engine in an
856 use ThreadedEngineService as the engine in an
856 IPython.frontend.frontendbase.FrontEndBase subclass to prevent
857 IPython.frontend.frontendbase.FrontEndBase subclass to prevent
857 block execution from blocking the GUI thread.
858 block execution from blocking the GUI thread.
858 """
859 """
859
860
860 zi.implements(IEngineBase)
861 zi.implements(IEngineBase)
861
862
862 def __init__(self, shellClass=Interpreter, mpi=None):
863 def __init__(self, shellClass=Interpreter, mpi=None):
863 EngineService.__init__(self, shellClass, mpi)
864 EngineService.__init__(self, shellClass, mpi)
864
865
866 def wrapped_execute(self, msg, lines):
867 """Wrap self.shell.execute to add extra information to tracebacks"""
868
869 try:
870 result = self.shell.execute(lines)
871 except Exception,e:
872 # This gives the following:
873 # et=exception class
874 # ev=exception class instance
875 # tb=traceback object
876 et,ev,tb = sys.exc_info()
877 # This call adds attributes to the exception value
878 et,ev,tb = self.shell.formatTraceback(et,ev,tb,msg)
879 # Add another attribute
880
881 # Create a new exception with the new attributes
882 e = et(ev._ipython_traceback_text)
883 e._ipython_engine_info = msg
884
885 # Re-raise
886 raise e
887
888 return result
889
865
890
866 def execute(self, lines):
891 def execute(self, lines):
867 # Only import this if we are going to use this class
892 # Only import this if we are going to use this class
868 from twisted.internet import threads
893 from twisted.internet import threads
869
894
870 msg = {'engineid':self.id,
895 msg = {'engineid':self.id,
871 'method':'execute',
896 'method':'execute',
872 'args':[lines]}
897 'args':[lines]}
873
898
874 d = threads.deferToThread(self.shell.execute, lines)
899 d = threads.deferToThread(self.wrapped_execute, msg, lines)
875 d.addCallback(self.addIDToResult)
900 d.addCallback(self.addIDToResult)
876 return d
901 return d
General Comments 0
You need to be logged in to leave comments. Login now