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