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