##// END OF EJS Templates
Fix traceback on exit.
Gael Varoquaux -
Show More
@@ -1,492 +1,493 b''
1 1 # encoding: utf-8 -*- test-case-name:
2 2 # FIXME: Need to add tests.
3 3 # ipython1.frontend.wx.tests.test_wx_frontend -*-
4 4
5 5 """Classes to provide a Wx frontend to the
6 6 IPython.kernel.core.interpreter.
7 7
8 8 This class inherits from ConsoleWidget, that provides a console-like
9 9 widget to provide a text-rendering widget suitable for a terminal.
10 10 """
11 11
12 12 __docformat__ = "restructuredtext en"
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Copyright (C) 2008 The IPython Development Team
16 16 #
17 17 # Distributed under the terms of the BSD License. The full license is in
18 18 # the file COPYING, distributed as part of this software.
19 19 #-------------------------------------------------------------------------------
20 20
21 21 #-------------------------------------------------------------------------------
22 22 # Imports
23 23 #-------------------------------------------------------------------------------
24 24
25 25 # Major library imports
26 26 import re
27 27 import __builtin__
28 28 from time import sleep
29 29 import sys
30 30 from threading import Lock
31 31 import string
32 32
33 33 import wx
34 34 from wx import stc
35 35
36 36 # Ipython-specific imports.
37 37 from IPython.frontend._process import PipedProcess
38 38 from console_widget import ConsoleWidget
39 39 from IPython.frontend.prefilterfrontend import PrefilterFrontEnd
40 40
41 41 #-------------------------------------------------------------------------------
42 42 # Constants
43 43 #-------------------------------------------------------------------------------
44 44
45 45 _COMPLETE_BUFFER_BG = '#FAFAF1' # Nice green
46 46 _INPUT_BUFFER_BG = '#FDFFD3' # Nice yellow
47 47 _ERROR_BG = '#FFF1F1' # Nice red
48 48
49 49 _COMPLETE_BUFFER_MARKER = 31
50 50 _ERROR_MARKER = 30
51 51 _INPUT_MARKER = 29
52 52
53 53 prompt_in1 = \
54 54 '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02$number\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02'
55 55
56 56 prompt_out = \
57 57 '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02$number\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02'
58 58
59 59 #-------------------------------------------------------------------------------
60 60 # Classes to implement the Wx frontend
61 61 #-------------------------------------------------------------------------------
62 62 class WxController(ConsoleWidget, PrefilterFrontEnd):
63 63 """Classes to provide a Wx frontend to the
64 64 IPython.kernel.core.interpreter.
65 65
66 66 This class inherits from ConsoleWidget, that provides a console-like
67 67 widget to provide a text-rendering widget suitable for a terminal.
68 68 """
69 69
70 70 output_prompt_template = string.Template(prompt_out)
71 71
72 72 input_prompt_template = string.Template(prompt_in1)
73 73
74 74 # Print debug info on what is happening to the console.
75 75 debug = False
76 76
77 77 # The title of the terminal, as captured through the ANSI escape
78 78 # sequences.
79 79 def _set_title(self, title):
80 80 return self.Parent.SetTitle(title)
81 81
82 82 def _get_title(self):
83 83 return self.Parent.GetTitle()
84 84
85 85 title = property(_get_title, _set_title)
86 86
87 87
88 88 # The buffer being edited.
89 89 # We are duplicating the definition here because of multiple
90 90 # inheritence
91 91 def _set_input_buffer(self, string):
92 92 ConsoleWidget._set_input_buffer(self, string)
93 93 self._colorize_input_buffer()
94 94
95 95 def _get_input_buffer(self):
96 96 """ Returns the text in current edit buffer.
97 97 """
98 98 return ConsoleWidget._get_input_buffer(self)
99 99
100 100 input_buffer = property(_get_input_buffer, _set_input_buffer)
101 101
102 102
103 103 #--------------------------------------------------------------------------
104 104 # Private Attributes
105 105 #--------------------------------------------------------------------------
106 106
107 107 # A flag governing the behavior of the input. Can be:
108 108 #
109 109 # 'readline' for readline-like behavior with a prompt
110 110 # and an edit buffer.
111 111 # 'raw_input' similar to readline, but triggered by a raw-input
112 112 # call. Can be used by subclasses to act differently.
113 113 # 'subprocess' for sending the raw input directly to a
114 114 # subprocess.
115 115 # 'buffering' for buffering of the input, that will be used
116 116 # when the input state switches back to another state.
117 117 _input_state = 'readline'
118 118
119 119 # Attribute to store reference to the pipes of a subprocess, if we
120 120 # are running any.
121 121 _running_process = False
122 122
123 123 # A queue for writing fast streams to the screen without flooding the
124 124 # event loop
125 125 _out_buffer = []
126 126
127 127 # A lock to lock the _out_buffer to make sure we don't empty it
128 128 # while it is being swapped
129 129 _out_buffer_lock = Lock()
130 130
131 131 _markers = dict()
132 132
133 133 #--------------------------------------------------------------------------
134 134 # Public API
135 135 #--------------------------------------------------------------------------
136 136
137 137 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
138 138 size=wx.DefaultSize, style=wx.CLIP_CHILDREN,
139 139 *args, **kwds):
140 140 """ Create Shell instance.
141 141 """
142 142 ConsoleWidget.__init__(self, parent, id, pos, size, style)
143 143 PrefilterFrontEnd.__init__(self, **kwds)
144 144
145 145 # Marker for complete buffer.
146 146 self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND,
147 147 background=_COMPLETE_BUFFER_BG)
148 148 # Marker for current input buffer.
149 149 self.MarkerDefine(_INPUT_MARKER, stc.STC_MARK_BACKGROUND,
150 150 background=_INPUT_BUFFER_BG)
151 151 # Marker for tracebacks.
152 152 self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND,
153 153 background=_ERROR_BG)
154 154
155 155 # A time for flushing the write buffer
156 156 BUFFER_FLUSH_TIMER_ID = 100
157 157 self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID)
158 158 wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush)
159 159
160 160 if 'debug' in kwds:
161 161 self.debug = kwds['debug']
162 162 kwds.pop('debug')
163 163
164 164 # Inject self in namespace, for debug
165 165 if self.debug:
166 166 self.shell.user_ns['self'] = self
167 167
168 168
169 169 def raw_input(self, prompt):
170 170 """ A replacement from python's raw_input.
171 171 """
172 172 self.new_prompt(prompt)
173 173 self._input_state = 'raw_input'
174 del self._cursor
174 if hasattr(self, '_cursor'):
175 del self._cursor
175 176 self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
176 177 self.waiting = True
177 178 self.__old_on_enter = self._on_enter
178 179 def my_on_enter():
179 180 self.waiting = False
180 181 self._on_enter = my_on_enter
181 182 # XXX: Busy waiting, ugly.
182 183 while self.waiting:
183 184 wx.Yield()
184 185 sleep(0.1)
185 186 self._on_enter = self.__old_on_enter
186 187 self._input_state = 'buffering'
187 188 self._cursor = wx.BusyCursor()
188 189 return self.input_buffer.rstrip('\n')
189 190
190 191
191 192 def system_call(self, command_string):
192 193 self._input_state = 'subprocess'
193 194 self._running_process = PipedProcess(command_string,
194 195 out_callback=self.buffered_write,
195 196 end_callback = self._end_system_call)
196 197 self._running_process.start()
197 198 # XXX: another one of these polling loops to have a blocking
198 199 # call
199 200 wx.Yield()
200 201 while self._running_process:
201 202 wx.Yield()
202 203 sleep(0.1)
203 204 # Be sure to flush the buffer.
204 205 self._buffer_flush(event=None)
205 206
206 207
207 208 def do_calltip(self):
208 209 """ Analyse current and displays useful calltip for it.
209 210 """
210 211 if self.debug:
211 212 print >>sys.__stdout__, "do_calltip"
212 213 separators = re.compile('[\s\{\}\[\]\(\)\= ,:]')
213 214 symbol = self.input_buffer
214 215 symbol_string = separators.split(symbol)[-1]
215 216 base_symbol_string = symbol_string.split('.')[0]
216 217 if base_symbol_string in self.shell.user_ns:
217 218 symbol = self.shell.user_ns[base_symbol_string]
218 219 elif base_symbol_string in self.shell.user_global_ns:
219 220 symbol = self.shell.user_global_ns[base_symbol_string]
220 221 elif base_symbol_string in __builtin__.__dict__:
221 222 symbol = __builtin__.__dict__[base_symbol_string]
222 223 else:
223 224 return False
224 225 try:
225 226 for name in symbol_string.split('.')[1:] + ['__doc__']:
226 227 symbol = getattr(symbol, name)
227 228 self.AutoCompCancel()
228 229 wx.Yield()
229 230 self.CallTipShow(self.GetCurrentPos(), symbol)
230 231 except:
231 232 # The retrieve symbol couldn't be converted to a string
232 233 pass
233 234
234 235
235 236 def _popup_completion(self, create=False):
236 237 """ Updates the popup completion menu if it exists. If create is
237 238 true, open the menu.
238 239 """
239 240 if self.debug:
240 241 print >>sys.__stdout__, "_popup_completion",
241 242 line = self.input_buffer
242 243 if (self.AutoCompActive() and not line[-1] == '.') \
243 244 or create==True:
244 245 suggestion, completions = self.complete(line)
245 246 offset=0
246 247 if completions:
247 248 complete_sep = re.compile('[\s\{\}\[\]\(\)\= ,:]')
248 249 residual = complete_sep.split(line)[-1]
249 250 offset = len(residual)
250 251 self.pop_completion(completions, offset=offset)
251 252 if self.debug:
252 253 print >>sys.__stdout__, completions
253 254
254 255
255 256 def buffered_write(self, text):
256 257 """ A write method for streams, that caches the stream in order
257 258 to avoid flooding the event loop.
258 259
259 260 This can be called outside of the main loop, in separate
260 261 threads.
261 262 """
262 263 self._out_buffer_lock.acquire()
263 264 self._out_buffer.append(text)
264 265 self._out_buffer_lock.release()
265 266 if not self._buffer_flush_timer.IsRunning():
266 267 wx.CallAfter(self._buffer_flush_timer.Start, 100) # milliseconds
267 268
268 269
269 270 #--------------------------------------------------------------------------
270 271 # LineFrontEnd interface
271 272 #--------------------------------------------------------------------------
272 273
273 274 def execute(self, python_string, raw_string=None):
274 275 self._input_state = 'buffering'
275 276 self.CallTipCancel()
276 277 self._cursor = wx.BusyCursor()
277 278 if raw_string is None:
278 279 raw_string = python_string
279 280 end_line = self.current_prompt_line \
280 281 + max(1, len(raw_string.split('\n'))-1)
281 282 for i in range(self.current_prompt_line, end_line):
282 283 if i in self._markers:
283 284 self.MarkerDeleteHandle(self._markers[i])
284 285 self._markers[i] = self.MarkerAdd(i, _COMPLETE_BUFFER_MARKER)
285 286 # Update the display:
286 287 wx.Yield()
287 288 self.GotoPos(self.GetLength())
288 289 PrefilterFrontEnd.execute(self, python_string, raw_string=raw_string)
289 290
290 291 def save_output_hooks(self):
291 292 self.__old_raw_input = __builtin__.raw_input
292 293 PrefilterFrontEnd.save_output_hooks(self)
293 294
294 295 def capture_output(self):
295 296 __builtin__.raw_input = self.raw_input
296 297 PrefilterFrontEnd.capture_output(self)
297 298
298 299
299 300 def release_output(self):
300 301 __builtin__.raw_input = self.__old_raw_input
301 302 PrefilterFrontEnd.release_output(self)
302 303
303 304
304 305 def after_execute(self):
305 306 PrefilterFrontEnd.after_execute(self)
306 307 # Clear the wait cursor
307 308 if hasattr(self, '_cursor'):
308 309 del self._cursor
309 310 self.SetCursor(wx.StockCursor(wx.CURSOR_CHAR))
310 311
311 312
312 313 def show_traceback(self):
313 314 start_line = self.GetCurrentLine()
314 315 PrefilterFrontEnd.show_traceback(self)
315 316 wx.Yield()
316 317 for i in range(start_line, self.GetCurrentLine()):
317 318 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
318 319
319 320
320 321 #--------------------------------------------------------------------------
321 322 # ConsoleWidget interface
322 323 #--------------------------------------------------------------------------
323 324
324 325 def new_prompt(self, prompt):
325 326 """ Display a new prompt, and start a new input buffer.
326 327 """
327 328 self._input_state = 'readline'
328 329 ConsoleWidget.new_prompt(self, prompt)
329 330 i = self.current_prompt_line
330 331 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
331 332
332 333
333 334 def write(self, *args, **kwargs):
334 335 # Avoid multiple inheritence, be explicit about which
335 336 # parent method class gets called
336 337 ConsoleWidget.write(self, *args, **kwargs)
337 338
338 339
339 340 def _on_key_down(self, event, skip=True):
340 341 """ Capture the character events, let the parent
341 342 widget handle them, and put our logic afterward.
342 343 """
343 344 # FIXME: This method needs to be broken down in smaller ones.
344 345 current_line_number = self.GetCurrentLine()
345 346 if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
346 347 # Capture Control-C
347 348 if self._input_state == 'subprocess':
348 349 if self.debug:
349 350 print >>sys.__stderr__, 'Killing running process'
350 351 self._running_process.process.kill()
351 352 elif self._input_state == 'buffering':
352 353 if self.debug:
353 354 print >>sys.__stderr__, 'Raising KeyboardInterrupt'
354 355 raise KeyboardInterrupt
355 356 # XXX: We need to make really sure we
356 357 # get back to a prompt.
357 358 elif self._input_state == 'subprocess' and (
358 359 ( event.KeyCode<256 and
359 360 not event.ControlDown() )
360 361 or
361 362 ( event.KeyCode in (ord('d'), ord('D')) and
362 363 event.ControlDown())):
363 364 # We are running a process, we redirect keys.
364 365 ConsoleWidget._on_key_down(self, event, skip=skip)
365 366 char = chr(event.KeyCode)
366 367 # Deal with some inconsistency in wx keycodes:
367 368 if char == '\r':
368 369 char = '\n'
369 370 elif not event.ShiftDown():
370 371 char = char.lower()
371 372 if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')):
372 373 char = '\04'
373 374 self._running_process.process.stdin.write(char)
374 375 self._running_process.process.stdin.flush()
375 376 elif event.KeyCode in (ord('('), 57):
376 377 # Calltips
377 378 event.Skip()
378 379 self.do_calltip()
379 380 elif self.AutoCompActive():
380 381 event.Skip()
381 382 if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE):
382 383 wx.CallAfter(self._popup_completion, create=True)
383 384 elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
384 385 wx.WXK_RIGHT):
385 386 wx.CallAfter(self._popup_completion)
386 387 else:
387 388 # Up history
388 389 if event.KeyCode == wx.WXK_UP and (
389 390 ( current_line_number == self.current_prompt_line and
390 391 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
391 392 or event.ControlDown() ):
392 393 new_buffer = self.get_history_previous(
393 394 self.input_buffer)
394 395 if new_buffer is not None:
395 396 self.input_buffer = new_buffer
396 397 if self.GetCurrentLine() > self.current_prompt_line:
397 398 # Go to first line, for seemless history up.
398 399 self.GotoPos(self.current_prompt_pos)
399 400 # Down history
400 401 elif event.KeyCode == wx.WXK_DOWN and (
401 402 ( current_line_number == self.LineCount -1 and
402 403 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
403 404 or event.ControlDown() ):
404 405 new_buffer = self.get_history_next()
405 406 if new_buffer is not None:
406 407 self.input_buffer = new_buffer
407 408 # Tab-completion
408 409 elif event.KeyCode == ord('\t'):
409 410 last_line = self.input_buffer.split('\n')[-1]
410 411 if not re.match(r'^\s*$', last_line):
411 412 self.complete_current_input()
412 413 else:
413 414 event.Skip()
414 415 else:
415 416 ConsoleWidget._on_key_down(self, event, skip=skip)
416 417
417 418
418 419 def _on_key_up(self, event, skip=True):
419 420 """ Called when any key is released.
420 421 """
421 422 if event.KeyCode in (59, ord('.')):
422 423 # Intercepting '.'
423 424 event.Skip()
424 425 self._popup_completion(create=True)
425 426 else:
426 427 ConsoleWidget._on_key_up(self, event, skip=skip)
427 428
428 429
429 430 def _on_enter(self):
430 431 """ Called on return key down, in readline input_state.
431 432 """
432 433 if self.debug:
433 434 print >>sys.__stdout__, repr(self.input_buffer)
434 435 PrefilterFrontEnd._on_enter(self)
435 436
436 437
437 438 #--------------------------------------------------------------------------
438 439 # Private API
439 440 #--------------------------------------------------------------------------
440 441
441 442 def _end_system_call(self):
442 443 """ Called at the end of a system call.
443 444 """
444 445 self._input_state = 'buffering'
445 446 self._running_process = False
446 447
447 448
448 449 def _buffer_flush(self, event):
449 450 """ Called by the timer to flush the write buffer.
450 451
451 452 This is always called in the mainloop, by the wx timer.
452 453 """
453 454 self._out_buffer_lock.acquire()
454 455 _out_buffer = self._out_buffer
455 456 self._out_buffer = []
456 457 self._out_buffer_lock.release()
457 458 self.write(''.join(_out_buffer), refresh=False)
458 459 self._buffer_flush_timer.Stop()
459 460
460 461 def _colorize_input_buffer(self):
461 462 """ Keep the input buffer lines at a bright color.
462 463 """
463 464 if not self._input_state in ('readline', 'raw_input'):
464 465 return
465 466 end_line = self.GetCurrentLine()
466 467 if not sys.platform == 'win32':
467 468 end_line += 1
468 469 for i in range(self.current_prompt_line, end_line):
469 470 if i in self._markers:
470 471 self.MarkerDeleteHandle(self._markers[i])
471 472 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
472 473
473 474
474 475 if __name__ == '__main__':
475 476 class MainWindow(wx.Frame):
476 477 def __init__(self, parent, id, title):
477 478 wx.Frame.__init__(self, parent, id, title, size=(300,250))
478 479 self._sizer = wx.BoxSizer(wx.VERTICAL)
479 480 self.shell = WxController(self)
480 481 self._sizer.Add(self.shell, 1, wx.EXPAND)
481 482 self.SetSizer(self._sizer)
482 483 self.SetAutoLayout(1)
483 484 self.Show(True)
484 485
485 486 app = wx.PySimpleApp()
486 487 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
487 488 frame.shell.SetFocus()
488 489 frame.SetSize((680, 460))
489 490 self = frame.shell
490 491
491 492 app.MainLoop()
492 493
General Comments 0
You need to be logged in to leave comments. Login now