##// END OF EJS Templates
tab management new/existing kernel....
Matthias BUSSONNIER -
Show More
@@ -1,638 +1,638 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool, Instance, Unicode
16 from IPython.utils.traitlets import Bool, Instance, Unicode
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that understands and ignores prompts.
25 """ A PygmentsHighlighter that understands and ignores prompts.
26 """
26 """
27
27
28 def __init__(self, frontend):
28 def __init__(self, frontend):
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 self._current_offset = 0
30 self._current_offset = 0
31 self._frontend = frontend
31 self._frontend = frontend
32 self.highlighting_on = False
32 self.highlighting_on = False
33
33
34 def highlightBlock(self, string):
34 def highlightBlock(self, string):
35 """ Highlight a block of text. Reimplemented to highlight selectively.
35 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """
36 """
37 if not self.highlighting_on:
37 if not self.highlighting_on:
38 return
38 return
39
39
40 # The input to this function is a unicode string that may contain
40 # The input to this function is a unicode string that may contain
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # the string as plain text so we can compare it.
42 # the string as plain text so we can compare it.
43 current_block = self.currentBlock()
43 current_block = self.currentBlock()
44 string = self._frontend._get_block_plain_text(current_block)
44 string = self._frontend._get_block_plain_text(current_block)
45
45
46 # Decide whether to check for the regular or continuation prompt.
46 # Decide whether to check for the regular or continuation prompt.
47 if current_block.contains(self._frontend._prompt_pos):
47 if current_block.contains(self._frontend._prompt_pos):
48 prompt = self._frontend._prompt
48 prompt = self._frontend._prompt
49 else:
49 else:
50 prompt = self._frontend._continuation_prompt
50 prompt = self._frontend._continuation_prompt
51
51
52 # Only highlight if we can identify a prompt, but make sure not to
52 # Only highlight if we can identify a prompt, but make sure not to
53 # highlight the prompt.
53 # highlight the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 super(FrontendHighlighter, self).highlightBlock(string)
57 super(FrontendHighlighter, self).highlightBlock(string)
58
58
59 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
60 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
61 """
61 """
62 old = self.highlighting_on
62 old = self.highlighting_on
63 self.highlighting_on = True
63 self.highlighting_on = True
64 super(FrontendHighlighter, self).rehighlightBlock(block)
64 super(FrontendHighlighter, self).rehighlightBlock(block)
65 self.highlighting_on = old
65 self.highlighting_on = old
66
66
67 def setFormat(self, start, count, format):
67 def setFormat(self, start, count, format):
68 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
69 """
69 """
70 start += self._current_offset
70 start += self._current_offset
71 super(FrontendHighlighter, self).setFormat(start, count, format)
71 super(FrontendHighlighter, self).setFormat(start, count, format)
72
72
73
73
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 """ A Qt frontend for a generic Python kernel.
75 """ A Qt frontend for a generic Python kernel.
76 """
76 """
77
77
78 # The text to show when the kernel is (re)started.
78 # The text to show when the kernel is (re)started.
79 banner = Unicode()
79 banner = Unicode()
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
82 # interrupt behavior.
83 custom_interrupt = Bool(False)
83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
84 custom_interrupt_requested = QtCore.Signal()
85
85
86 # An option and corresponding signals for overriding the default kernel
86 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
87 # restart behavior.
88 custom_restart = Bool(False)
88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
90 custom_restart_requested = QtCore.Signal()
91
91
92 # Whether to automatically show calltips on open-parentheses.
92 # Whether to automatically show calltips on open-parentheses.
93 enable_calltips = Bool(True, config=True,
93 enable_calltips = Bool(True, config=True,
94 help="Whether to draw information calltips on open-parentheses.")
94 help="Whether to draw information calltips on open-parentheses.")
95
95
96 # Emitted when a user visible 'execute_request' has been submitted to the
96 # Emitted when a user visible 'execute_request' has been submitted to the
97 # kernel from the FrontendWidget. Contains the code to be executed.
97 # kernel from the FrontendWidget. Contains the code to be executed.
98 executing = QtCore.Signal(object)
98 executing = QtCore.Signal(object)
99
99
100 # Emitted when a user-visible 'execute_reply' has been received from the
100 # Emitted when a user-visible 'execute_reply' has been received from the
101 # kernel and processed by the FrontendWidget. Contains the response message.
101 # kernel and processed by the FrontendWidget. Contains the response message.
102 executed = QtCore.Signal(object)
102 executed = QtCore.Signal(object)
103
103
104 # Emitted when an exit request has been received from the kernel.
104 # Emitted when an exit request has been received from the kernel.
105 exit_requested = QtCore.Signal()
105 exit_requested = QtCore.Signal(object)
106
106
107 # Protected class variables.
107 # Protected class variables.
108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
111 _input_splitter_class = InputSplitter
111 _input_splitter_class = InputSplitter
112 _local_kernel = False
112 _local_kernel = False
113 _highlighter = Instance(FrontendHighlighter)
113 _highlighter = Instance(FrontendHighlighter)
114
114
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116 # 'object' interface
116 # 'object' interface
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118
118
119 def __init__(self, *args, **kw):
119 def __init__(self, *args, **kw):
120 super(FrontendWidget, self).__init__(*args, **kw)
120 super(FrontendWidget, self).__init__(*args, **kw)
121
121
122 # FrontendWidget protected variables.
122 # FrontendWidget protected variables.
123 self._bracket_matcher = BracketMatcher(self._control)
123 self._bracket_matcher = BracketMatcher(self._control)
124 self._call_tip_widget = CallTipWidget(self._control)
124 self._call_tip_widget = CallTipWidget(self._control)
125 self._completion_lexer = CompletionLexer(PythonLexer())
125 self._completion_lexer = CompletionLexer(PythonLexer())
126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
127 self._hidden = False
127 self._hidden = False
128 self._highlighter = FrontendHighlighter(self)
128 self._highlighter = FrontendHighlighter(self)
129 self._input_splitter = self._input_splitter_class(input_mode='cell')
129 self._input_splitter = self._input_splitter_class(input_mode='cell')
130 self._kernel_manager = None
130 self._kernel_manager = None
131 self._request_info = {}
131 self._request_info = {}
132
132
133 # Configure the ConsoleWidget.
133 # Configure the ConsoleWidget.
134 self.tab_width = 4
134 self.tab_width = 4
135 self._set_continuation_prompt('... ')
135 self._set_continuation_prompt('... ')
136
136
137 # Configure the CallTipWidget.
137 # Configure the CallTipWidget.
138 self._call_tip_widget.setFont(self.font)
138 self._call_tip_widget.setFont(self.font)
139 self.font_changed.connect(self._call_tip_widget.setFont)
139 self.font_changed.connect(self._call_tip_widget.setFont)
140
140
141 # Configure actions.
141 # Configure actions.
142 action = self._copy_raw_action
142 action = self._copy_raw_action
143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
144 action.setEnabled(False)
144 action.setEnabled(False)
145 action.setShortcut(QtGui.QKeySequence(key))
145 action.setShortcut(QtGui.QKeySequence(key))
146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
147 action.triggered.connect(self.copy_raw)
147 action.triggered.connect(self.copy_raw)
148 self.copy_available.connect(action.setEnabled)
148 self.copy_available.connect(action.setEnabled)
149 self.addAction(action)
149 self.addAction(action)
150
150
151 # Connect signal handlers.
151 # Connect signal handlers.
152 document = self._control.document()
152 document = self._control.document()
153 document.contentsChange.connect(self._document_contents_change)
153 document.contentsChange.connect(self._document_contents_change)
154
154
155 # Set flag for whether we are connected via localhost.
155 # Set flag for whether we are connected via localhost.
156 self._local_kernel = kw.get('local_kernel',
156 self._local_kernel = kw.get('local_kernel',
157 FrontendWidget._local_kernel)
157 FrontendWidget._local_kernel)
158
158
159 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
160 # 'ConsoleWidget' public interface
160 # 'ConsoleWidget' public interface
161 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
162
162
163 def copy(self):
163 def copy(self):
164 """ Copy the currently selected text to the clipboard, removing prompts.
164 """ Copy the currently selected text to the clipboard, removing prompts.
165 """
165 """
166 text = self._control.textCursor().selection().toPlainText()
166 text = self._control.textCursor().selection().toPlainText()
167 if text:
167 if text:
168 lines = map(transform_classic_prompt, text.splitlines())
168 lines = map(transform_classic_prompt, text.splitlines())
169 text = '\n'.join(lines)
169 text = '\n'.join(lines)
170 QtGui.QApplication.clipboard().setText(text)
170 QtGui.QApplication.clipboard().setText(text)
171
171
172 #---------------------------------------------------------------------------
172 #---------------------------------------------------------------------------
173 # 'ConsoleWidget' abstract interface
173 # 'ConsoleWidget' abstract interface
174 #---------------------------------------------------------------------------
174 #---------------------------------------------------------------------------
175
175
176 def _is_complete(self, source, interactive):
176 def _is_complete(self, source, interactive):
177 """ Returns whether 'source' can be completely processed and a new
177 """ Returns whether 'source' can be completely processed and a new
178 prompt created. When triggered by an Enter/Return key press,
178 prompt created. When triggered by an Enter/Return key press,
179 'interactive' is True; otherwise, it is False.
179 'interactive' is True; otherwise, it is False.
180 """
180 """
181 complete = self._input_splitter.push(source)
181 complete = self._input_splitter.push(source)
182 if interactive:
182 if interactive:
183 complete = not self._input_splitter.push_accepts_more()
183 complete = not self._input_splitter.push_accepts_more()
184 return complete
184 return complete
185
185
186 def _execute(self, source, hidden):
186 def _execute(self, source, hidden):
187 """ Execute 'source'. If 'hidden', do not show any output.
187 """ Execute 'source'. If 'hidden', do not show any output.
188
188
189 See parent class :meth:`execute` docstring for full details.
189 See parent class :meth:`execute` docstring for full details.
190 """
190 """
191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
193 self._hidden = hidden
193 self._hidden = hidden
194 if not hidden:
194 if not hidden:
195 self.executing.emit(source)
195 self.executing.emit(source)
196
196
197 def _prompt_started_hook(self):
197 def _prompt_started_hook(self):
198 """ Called immediately after a new prompt is displayed.
198 """ Called immediately after a new prompt is displayed.
199 """
199 """
200 if not self._reading:
200 if not self._reading:
201 self._highlighter.highlighting_on = True
201 self._highlighter.highlighting_on = True
202
202
203 def _prompt_finished_hook(self):
203 def _prompt_finished_hook(self):
204 """ Called immediately after a prompt is finished, i.e. when some input
204 """ Called immediately after a prompt is finished, i.e. when some input
205 will be processed and a new prompt displayed.
205 will be processed and a new prompt displayed.
206 """
206 """
207 # Flush all state from the input splitter so the next round of
207 # Flush all state from the input splitter so the next round of
208 # reading input starts with a clean buffer.
208 # reading input starts with a clean buffer.
209 self._input_splitter.reset()
209 self._input_splitter.reset()
210
210
211 if not self._reading:
211 if not self._reading:
212 self._highlighter.highlighting_on = False
212 self._highlighter.highlighting_on = False
213
213
214 def _tab_pressed(self):
214 def _tab_pressed(self):
215 """ Called when the tab key is pressed. Returns whether to continue
215 """ Called when the tab key is pressed. Returns whether to continue
216 processing the event.
216 processing the event.
217 """
217 """
218 # Perform tab completion if:
218 # Perform tab completion if:
219 # 1) The cursor is in the input buffer.
219 # 1) The cursor is in the input buffer.
220 # 2) There is a non-whitespace character before the cursor.
220 # 2) There is a non-whitespace character before the cursor.
221 text = self._get_input_buffer_cursor_line()
221 text = self._get_input_buffer_cursor_line()
222 if text is None:
222 if text is None:
223 return False
223 return False
224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
225 if complete:
225 if complete:
226 self._complete()
226 self._complete()
227 return not complete
227 return not complete
228
228
229 #---------------------------------------------------------------------------
229 #---------------------------------------------------------------------------
230 # 'ConsoleWidget' protected interface
230 # 'ConsoleWidget' protected interface
231 #---------------------------------------------------------------------------
231 #---------------------------------------------------------------------------
232
232
233 def _context_menu_make(self, pos):
233 def _context_menu_make(self, pos):
234 """ Reimplemented to add an action for raw copy.
234 """ Reimplemented to add an action for raw copy.
235 """
235 """
236 menu = super(FrontendWidget, self)._context_menu_make(pos)
236 menu = super(FrontendWidget, self)._context_menu_make(pos)
237 for before_action in menu.actions():
237 for before_action in menu.actions():
238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
239 QtGui.QKeySequence.ExactMatch:
239 QtGui.QKeySequence.ExactMatch:
240 menu.insertAction(before_action, self._copy_raw_action)
240 menu.insertAction(before_action, self._copy_raw_action)
241 break
241 break
242 return menu
242 return menu
243
243
244 def _event_filter_console_keypress(self, event):
244 def _event_filter_console_keypress(self, event):
245 """ Reimplemented for execution interruption and smart backspace.
245 """ Reimplemented for execution interruption and smart backspace.
246 """
246 """
247 key = event.key()
247 key = event.key()
248 if self._control_key_down(event.modifiers(), include_command=False):
248 if self._control_key_down(event.modifiers(), include_command=False):
249
249
250 if key == QtCore.Qt.Key_C and self._executing:
250 if key == QtCore.Qt.Key_C and self._executing:
251 self.interrupt_kernel()
251 self.interrupt_kernel()
252 return True
252 return True
253
253
254 elif key == QtCore.Qt.Key_Period:
254 elif key == QtCore.Qt.Key_Period:
255 message = 'Are you sure you want to restart the kernel?'
255 message = 'Are you sure you want to restart the kernel?'
256 self.restart_kernel(message, now=False)
256 self.restart_kernel(message, now=False)
257 return True
257 return True
258
258
259 elif not event.modifiers() & QtCore.Qt.AltModifier:
259 elif not event.modifiers() & QtCore.Qt.AltModifier:
260
260
261 # Smart backspace: remove four characters in one backspace if:
261 # Smart backspace: remove four characters in one backspace if:
262 # 1) everything left of the cursor is whitespace
262 # 1) everything left of the cursor is whitespace
263 # 2) the four characters immediately left of the cursor are spaces
263 # 2) the four characters immediately left of the cursor are spaces
264 if key == QtCore.Qt.Key_Backspace:
264 if key == QtCore.Qt.Key_Backspace:
265 col = self._get_input_buffer_cursor_column()
265 col = self._get_input_buffer_cursor_column()
266 cursor = self._control.textCursor()
266 cursor = self._control.textCursor()
267 if col > 3 and not cursor.hasSelection():
267 if col > 3 and not cursor.hasSelection():
268 text = self._get_input_buffer_cursor_line()[:col]
268 text = self._get_input_buffer_cursor_line()[:col]
269 if text.endswith(' ') and not text.strip():
269 if text.endswith(' ') and not text.strip():
270 cursor.movePosition(QtGui.QTextCursor.Left,
270 cursor.movePosition(QtGui.QTextCursor.Left,
271 QtGui.QTextCursor.KeepAnchor, 4)
271 QtGui.QTextCursor.KeepAnchor, 4)
272 cursor.removeSelectedText()
272 cursor.removeSelectedText()
273 return True
273 return True
274
274
275 return super(FrontendWidget, self)._event_filter_console_keypress(event)
275 return super(FrontendWidget, self)._event_filter_console_keypress(event)
276
276
277 def _insert_continuation_prompt(self, cursor):
277 def _insert_continuation_prompt(self, cursor):
278 """ Reimplemented for auto-indentation.
278 """ Reimplemented for auto-indentation.
279 """
279 """
280 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
280 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
281 cursor.insertText(' ' * self._input_splitter.indent_spaces)
281 cursor.insertText(' ' * self._input_splitter.indent_spaces)
282
282
283 #---------------------------------------------------------------------------
283 #---------------------------------------------------------------------------
284 # 'BaseFrontendMixin' abstract interface
284 # 'BaseFrontendMixin' abstract interface
285 #---------------------------------------------------------------------------
285 #---------------------------------------------------------------------------
286
286
287 def _handle_complete_reply(self, rep):
287 def _handle_complete_reply(self, rep):
288 """ Handle replies for tab completion.
288 """ Handle replies for tab completion.
289 """
289 """
290 self.log.debug("complete: %s", rep.get('content', ''))
290 self.log.debug("complete: %s", rep.get('content', ''))
291 cursor = self._get_cursor()
291 cursor = self._get_cursor()
292 info = self._request_info.get('complete')
292 info = self._request_info.get('complete')
293 if info and info.id == rep['parent_header']['msg_id'] and \
293 if info and info.id == rep['parent_header']['msg_id'] and \
294 info.pos == cursor.position():
294 info.pos == cursor.position():
295 text = '.'.join(self._get_context())
295 text = '.'.join(self._get_context())
296 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
296 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
297 self._complete_with_items(cursor, rep['content']['matches'])
297 self._complete_with_items(cursor, rep['content']['matches'])
298
298
299 def _handle_execute_reply(self, msg):
299 def _handle_execute_reply(self, msg):
300 """ Handles replies for code execution.
300 """ Handles replies for code execution.
301 """
301 """
302 self.log.debug("execute: %s", msg.get('content', ''))
302 self.log.debug("execute: %s", msg.get('content', ''))
303 info = self._request_info.get('execute')
303 info = self._request_info.get('execute')
304 if info and info.id == msg['parent_header']['msg_id'] and \
304 if info and info.id == msg['parent_header']['msg_id'] and \
305 info.kind == 'user' and not self._hidden:
305 info.kind == 'user' and not self._hidden:
306 # Make sure that all output from the SUB channel has been processed
306 # Make sure that all output from the SUB channel has been processed
307 # before writing a new prompt.
307 # before writing a new prompt.
308 self.kernel_manager.sub_channel.flush()
308 self.kernel_manager.sub_channel.flush()
309
309
310 # Reset the ANSI style information to prevent bad text in stdout
310 # Reset the ANSI style information to prevent bad text in stdout
311 # from messing up our colors. We're not a true terminal so we're
311 # from messing up our colors. We're not a true terminal so we're
312 # allowed to do this.
312 # allowed to do this.
313 if self.ansi_codes:
313 if self.ansi_codes:
314 self._ansi_processor.reset_sgr()
314 self._ansi_processor.reset_sgr()
315
315
316 content = msg['content']
316 content = msg['content']
317 status = content['status']
317 status = content['status']
318 if status == 'ok':
318 if status == 'ok':
319 self._process_execute_ok(msg)
319 self._process_execute_ok(msg)
320 elif status == 'error':
320 elif status == 'error':
321 self._process_execute_error(msg)
321 self._process_execute_error(msg)
322 elif status == 'abort':
322 elif status == 'abort':
323 self._process_execute_abort(msg)
323 self._process_execute_abort(msg)
324
324
325 self._show_interpreter_prompt_for_reply(msg)
325 self._show_interpreter_prompt_for_reply(msg)
326 self.executed.emit(msg)
326 self.executed.emit(msg)
327 else:
327 else:
328 super(FrontendWidget, self)._handle_execute_reply(msg)
328 super(FrontendWidget, self)._handle_execute_reply(msg)
329
329
330 def _handle_input_request(self, msg):
330 def _handle_input_request(self, msg):
331 """ Handle requests for raw_input.
331 """ Handle requests for raw_input.
332 """
332 """
333 self.log.debug("input: %s", msg.get('content', ''))
333 self.log.debug("input: %s", msg.get('content', ''))
334 if self._hidden:
334 if self._hidden:
335 raise RuntimeError('Request for raw input during hidden execution.')
335 raise RuntimeError('Request for raw input during hidden execution.')
336
336
337 # Make sure that all output from the SUB channel has been processed
337 # Make sure that all output from the SUB channel has been processed
338 # before entering readline mode.
338 # before entering readline mode.
339 self.kernel_manager.sub_channel.flush()
339 self.kernel_manager.sub_channel.flush()
340
340
341 def callback(line):
341 def callback(line):
342 self.kernel_manager.stdin_channel.input(line)
342 self.kernel_manager.stdin_channel.input(line)
343 self._readline(msg['content']['prompt'], callback=callback)
343 self._readline(msg['content']['prompt'], callback=callback)
344
344
345 def _handle_kernel_died(self, since_last_heartbeat):
345 def _handle_kernel_died(self, since_last_heartbeat):
346 """ Handle the kernel's death by asking if the user wants to restart.
346 """ Handle the kernel's death by asking if the user wants to restart.
347 """
347 """
348 self.log.debug("kernel died: %s", since_last_heartbeat)
348 self.log.debug("kernel died: %s", since_last_heartbeat)
349 if self.custom_restart:
349 if self.custom_restart:
350 self.custom_restart_kernel_died.emit(since_last_heartbeat)
350 self.custom_restart_kernel_died.emit(since_last_heartbeat)
351 else:
351 else:
352 message = 'The kernel heartbeat has been inactive for %.2f ' \
352 message = 'The kernel heartbeat has been inactive for %.2f ' \
353 'seconds. Do you want to restart the kernel? You may ' \
353 'seconds. Do you want to restart the kernel? You may ' \
354 'first want to check the network connection.' % \
354 'first want to check the network connection.' % \
355 since_last_heartbeat
355 since_last_heartbeat
356 self.restart_kernel(message, now=True)
356 self.restart_kernel(message, now=True)
357
357
358 def _handle_object_info_reply(self, rep):
358 def _handle_object_info_reply(self, rep):
359 """ Handle replies for call tips.
359 """ Handle replies for call tips.
360 """
360 """
361 self.log.debug("oinfo: %s", rep.get('content', ''))
361 self.log.debug("oinfo: %s", rep.get('content', ''))
362 cursor = self._get_cursor()
362 cursor = self._get_cursor()
363 info = self._request_info.get('call_tip')
363 info = self._request_info.get('call_tip')
364 if info and info.id == rep['parent_header']['msg_id'] and \
364 if info and info.id == rep['parent_header']['msg_id'] and \
365 info.pos == cursor.position():
365 info.pos == cursor.position():
366 # Get the information for a call tip. For now we format the call
366 # Get the information for a call tip. For now we format the call
367 # line as string, later we can pass False to format_call and
367 # line as string, later we can pass False to format_call and
368 # syntax-highlight it ourselves for nicer formatting in the
368 # syntax-highlight it ourselves for nicer formatting in the
369 # calltip.
369 # calltip.
370 content = rep['content']
370 content = rep['content']
371 # if this is from pykernel, 'docstring' will be the only key
371 # if this is from pykernel, 'docstring' will be the only key
372 if content.get('ismagic', False):
372 if content.get('ismagic', False):
373 # Don't generate a call-tip for magics. Ideally, we should
373 # Don't generate a call-tip for magics. Ideally, we should
374 # generate a tooltip, but not on ( like we do for actual
374 # generate a tooltip, but not on ( like we do for actual
375 # callables.
375 # callables.
376 call_info, doc = None, None
376 call_info, doc = None, None
377 else:
377 else:
378 call_info, doc = call_tip(content, format_call=True)
378 call_info, doc = call_tip(content, format_call=True)
379 if call_info or doc:
379 if call_info or doc:
380 self._call_tip_widget.show_call_info(call_info, doc)
380 self._call_tip_widget.show_call_info(call_info, doc)
381
381
382 def _handle_pyout(self, msg):
382 def _handle_pyout(self, msg):
383 """ Handle display hook output.
383 """ Handle display hook output.
384 """
384 """
385 self.log.debug("pyout: %s", msg.get('content', ''))
385 self.log.debug("pyout: %s", msg.get('content', ''))
386 if not self._hidden and self._is_from_this_session(msg):
386 if not self._hidden and self._is_from_this_session(msg):
387 text = msg['content']['data']
387 text = msg['content']['data']
388 self._append_plain_text(text + '\n', before_prompt=True)
388 self._append_plain_text(text + '\n', before_prompt=True)
389
389
390 def _handle_stream(self, msg):
390 def _handle_stream(self, msg):
391 """ Handle stdout, stderr, and stdin.
391 """ Handle stdout, stderr, and stdin.
392 """
392 """
393 self.log.debug("stream: %s", msg.get('content', ''))
393 self.log.debug("stream: %s", msg.get('content', ''))
394 if not self._hidden and self._is_from_this_session(msg):
394 if not self._hidden and self._is_from_this_session(msg):
395 # Most consoles treat tabs as being 8 space characters. Convert tabs
395 # Most consoles treat tabs as being 8 space characters. Convert tabs
396 # to spaces so that output looks as expected regardless of this
396 # to spaces so that output looks as expected regardless of this
397 # widget's tab width.
397 # widget's tab width.
398 text = msg['content']['data'].expandtabs(8)
398 text = msg['content']['data'].expandtabs(8)
399
399
400 self._append_plain_text(text, before_prompt=True)
400 self._append_plain_text(text, before_prompt=True)
401 self._control.moveCursor(QtGui.QTextCursor.End)
401 self._control.moveCursor(QtGui.QTextCursor.End)
402
402
403 def _handle_shutdown_reply(self, msg):
403 def _handle_shutdown_reply(self, msg):
404 """ Handle shutdown signal, only if from other console.
404 """ Handle shutdown signal, only if from other console.
405 """
405 """
406 self.log.debug("shutdown: %s", msg.get('content', ''))
406 self.log.debug("shutdown: %s", msg.get('content', ''))
407 if not self._hidden and not self._is_from_this_session(msg):
407 if not self._hidden and not self._is_from_this_session(msg):
408 if self._local_kernel:
408 if self._local_kernel:
409 if not msg['content']['restart']:
409 if not msg['content']['restart']:
410 sys.exit(0)
410 sys.exit(0)
411 else:
411 else:
412 # we just got notified of a restart!
412 # we just got notified of a restart!
413 time.sleep(0.25) # wait 1/4 sec to reset
413 time.sleep(0.25) # wait 1/4 sec to reset
414 # lest the request for a new prompt
414 # lest the request for a new prompt
415 # goes to the old kernel
415 # goes to the old kernel
416 self.reset()
416 self.reset()
417 else: # remote kernel, prompt on Kernel shutdown/reset
417 else: # remote kernel, prompt on Kernel shutdown/reset
418 title = self.window().windowTitle()
418 title = self.window().windowTitle()
419 if not msg['content']['restart']:
419 if not msg['content']['restart']:
420 reply = QtGui.QMessageBox.question(self, title,
420 reply = QtGui.QMessageBox.question(self, title,
421 "Kernel has been shutdown permanently. "
421 "Kernel has been shutdown permanently. "
422 "Close the Console?",
422 "Close the Console?",
423 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
423 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
424 if reply == QtGui.QMessageBox.Yes:
424 if reply == QtGui.QMessageBox.Yes:
425 sys.exit(0)
425 self.exit_requested.emit(self)
426 else:
426 else:
427 reply = QtGui.QMessageBox.question(self, title,
427 reply = QtGui.QMessageBox.question(self, title,
428 "Kernel has been reset. Clear the Console?",
428 "Kernel has been reset. Clear the Console?",
429 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
429 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
430 if reply == QtGui.QMessageBox.Yes:
430 if reply == QtGui.QMessageBox.Yes:
431 time.sleep(0.25) # wait 1/4 sec to reset
431 time.sleep(0.25) # wait 1/4 sec to reset
432 # lest the request for a new prompt
432 # lest the request for a new prompt
433 # goes to the old kernel
433 # goes to the old kernel
434 self.reset()
434 self.reset()
435
435
436 def _started_channels(self):
436 def _started_channels(self):
437 """ Called when the KernelManager channels have started listening or
437 """ Called when the KernelManager channels have started listening or
438 when the frontend is assigned an already listening KernelManager.
438 when the frontend is assigned an already listening KernelManager.
439 """
439 """
440 self.reset()
440 self.reset()
441
441
442 #---------------------------------------------------------------------------
442 #---------------------------------------------------------------------------
443 # 'FrontendWidget' public interface
443 # 'FrontendWidget' public interface
444 #---------------------------------------------------------------------------
444 #---------------------------------------------------------------------------
445
445
446 def copy_raw(self):
446 def copy_raw(self):
447 """ Copy the currently selected text to the clipboard without attempting
447 """ Copy the currently selected text to the clipboard without attempting
448 to remove prompts or otherwise alter the text.
448 to remove prompts or otherwise alter the text.
449 """
449 """
450 self._control.copy()
450 self._control.copy()
451
451
452 def execute_file(self, path, hidden=False):
452 def execute_file(self, path, hidden=False):
453 """ Attempts to execute file with 'path'. If 'hidden', no output is
453 """ Attempts to execute file with 'path'. If 'hidden', no output is
454 shown.
454 shown.
455 """
455 """
456 self.execute('execfile(%r)' % path, hidden=hidden)
456 self.execute('execfile(%r)' % path, hidden=hidden)
457
457
458 def interrupt_kernel(self):
458 def interrupt_kernel(self):
459 """ Attempts to interrupt the running kernel.
459 """ Attempts to interrupt the running kernel.
460 """
460 """
461 if self.custom_interrupt:
461 if self.custom_interrupt:
462 self.custom_interrupt_requested.emit()
462 self.custom_interrupt_requested.emit()
463 elif self.kernel_manager.has_kernel:
463 elif self.kernel_manager.has_kernel:
464 self.kernel_manager.interrupt_kernel()
464 self.kernel_manager.interrupt_kernel()
465 else:
465 else:
466 self._append_plain_text('Kernel process is either remote or '
466 self._append_plain_text('Kernel process is either remote or '
467 'unspecified. Cannot interrupt.\n')
467 'unspecified. Cannot interrupt.\n')
468
468
469 def reset(self):
469 def reset(self):
470 """ Resets the widget to its initial state. Similar to ``clear``, but
470 """ Resets the widget to its initial state. Similar to ``clear``, but
471 also re-writes the banner and aborts execution if necessary.
471 also re-writes the banner and aborts execution if necessary.
472 """
472 """
473 if self._executing:
473 if self._executing:
474 self._executing = False
474 self._executing = False
475 self._request_info['execute'] = None
475 self._request_info['execute'] = None
476 self._reading = False
476 self._reading = False
477 self._highlighter.highlighting_on = False
477 self._highlighter.highlighting_on = False
478
478
479 self._control.clear()
479 self._control.clear()
480 self._append_plain_text(self.banner)
480 self._append_plain_text(self.banner)
481 self._show_interpreter_prompt()
481 self._show_interpreter_prompt()
482
482
483 def restart_kernel(self, message, now=False):
483 def restart_kernel(self, message, now=False):
484 """ Attempts to restart the running kernel.
484 """ Attempts to restart the running kernel.
485 """
485 """
486 # FIXME: now should be configurable via a checkbox in the dialog. Right
486 # FIXME: now should be configurable via a checkbox in the dialog. Right
487 # now at least the heartbeat path sets it to True and the manual restart
487 # now at least the heartbeat path sets it to True and the manual restart
488 # to False. But those should just be the pre-selected states of a
488 # to False. But those should just be the pre-selected states of a
489 # checkbox that the user could override if so desired. But I don't know
489 # checkbox that the user could override if so desired. But I don't know
490 # enough Qt to go implementing the checkbox now.
490 # enough Qt to go implementing the checkbox now.
491
491
492 if self.custom_restart:
492 if self.custom_restart:
493 self.custom_restart_requested.emit()
493 self.custom_restart_requested.emit()
494
494
495 elif self.kernel_manager.has_kernel:
495 elif self.kernel_manager.has_kernel:
496 # Pause the heart beat channel to prevent further warnings.
496 # Pause the heart beat channel to prevent further warnings.
497 self.kernel_manager.hb_channel.pause()
497 self.kernel_manager.hb_channel.pause()
498
498
499 # Prompt the user to restart the kernel. Un-pause the heartbeat if
499 # Prompt the user to restart the kernel. Un-pause the heartbeat if
500 # they decline. (If they accept, the heartbeat will be un-paused
500 # they decline. (If they accept, the heartbeat will be un-paused
501 # automatically when the kernel is restarted.)
501 # automatically when the kernel is restarted.)
502 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
502 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
503 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
503 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
504 message, buttons)
504 message, buttons)
505 if result == QtGui.QMessageBox.Yes:
505 if result == QtGui.QMessageBox.Yes:
506 try:
506 try:
507 self.kernel_manager.restart_kernel(now=now)
507 self.kernel_manager.restart_kernel(now=now)
508 except RuntimeError:
508 except RuntimeError:
509 self._append_plain_text('Kernel started externally. '
509 self._append_plain_text('Kernel started externally. '
510 'Cannot restart.\n')
510 'Cannot restart.\n')
511 else:
511 else:
512 self.reset()
512 self.reset()
513 else:
513 else:
514 self.kernel_manager.hb_channel.unpause()
514 self.kernel_manager.hb_channel.unpause()
515
515
516 else:
516 else:
517 self._append_plain_text('Kernel process is either remote or '
517 self._append_plain_text('Kernel process is either remote or '
518 'unspecified. Cannot restart.\n')
518 'unspecified. Cannot restart.\n')
519
519
520 #---------------------------------------------------------------------------
520 #---------------------------------------------------------------------------
521 # 'FrontendWidget' protected interface
521 # 'FrontendWidget' protected interface
522 #---------------------------------------------------------------------------
522 #---------------------------------------------------------------------------
523
523
524 def _call_tip(self):
524 def _call_tip(self):
525 """ Shows a call tip, if appropriate, at the current cursor location.
525 """ Shows a call tip, if appropriate, at the current cursor location.
526 """
526 """
527 # Decide if it makes sense to show a call tip
527 # Decide if it makes sense to show a call tip
528 if not self.enable_calltips:
528 if not self.enable_calltips:
529 return False
529 return False
530 cursor = self._get_cursor()
530 cursor = self._get_cursor()
531 cursor.movePosition(QtGui.QTextCursor.Left)
531 cursor.movePosition(QtGui.QTextCursor.Left)
532 if cursor.document().characterAt(cursor.position()) != '(':
532 if cursor.document().characterAt(cursor.position()) != '(':
533 return False
533 return False
534 context = self._get_context(cursor)
534 context = self._get_context(cursor)
535 if not context:
535 if not context:
536 return False
536 return False
537
537
538 # Send the metadata request to the kernel
538 # Send the metadata request to the kernel
539 name = '.'.join(context)
539 name = '.'.join(context)
540 msg_id = self.kernel_manager.shell_channel.object_info(name)
540 msg_id = self.kernel_manager.shell_channel.object_info(name)
541 pos = self._get_cursor().position()
541 pos = self._get_cursor().position()
542 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
542 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
543 return True
543 return True
544
544
545 def _complete(self):
545 def _complete(self):
546 """ Performs completion at the current cursor location.
546 """ Performs completion at the current cursor location.
547 """
547 """
548 context = self._get_context()
548 context = self._get_context()
549 if context:
549 if context:
550 # Send the completion request to the kernel
550 # Send the completion request to the kernel
551 msg_id = self.kernel_manager.shell_channel.complete(
551 msg_id = self.kernel_manager.shell_channel.complete(
552 '.'.join(context), # text
552 '.'.join(context), # text
553 self._get_input_buffer_cursor_line(), # line
553 self._get_input_buffer_cursor_line(), # line
554 self._get_input_buffer_cursor_column(), # cursor_pos
554 self._get_input_buffer_cursor_column(), # cursor_pos
555 self.input_buffer) # block
555 self.input_buffer) # block
556 pos = self._get_cursor().position()
556 pos = self._get_cursor().position()
557 info = self._CompletionRequest(msg_id, pos)
557 info = self._CompletionRequest(msg_id, pos)
558 self._request_info['complete'] = info
558 self._request_info['complete'] = info
559
559
560 def _get_context(self, cursor=None):
560 def _get_context(self, cursor=None):
561 """ Gets the context for the specified cursor (or the current cursor
561 """ Gets the context for the specified cursor (or the current cursor
562 if none is specified).
562 if none is specified).
563 """
563 """
564 if cursor is None:
564 if cursor is None:
565 cursor = self._get_cursor()
565 cursor = self._get_cursor()
566 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
566 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
567 QtGui.QTextCursor.KeepAnchor)
567 QtGui.QTextCursor.KeepAnchor)
568 text = cursor.selection().toPlainText()
568 text = cursor.selection().toPlainText()
569 return self._completion_lexer.get_context(text)
569 return self._completion_lexer.get_context(text)
570
570
571 def _process_execute_abort(self, msg):
571 def _process_execute_abort(self, msg):
572 """ Process a reply for an aborted execution request.
572 """ Process a reply for an aborted execution request.
573 """
573 """
574 self._append_plain_text("ERROR: execution aborted\n")
574 self._append_plain_text("ERROR: execution aborted\n")
575
575
576 def _process_execute_error(self, msg):
576 def _process_execute_error(self, msg):
577 """ Process a reply for an execution request that resulted in an error.
577 """ Process a reply for an execution request that resulted in an error.
578 """
578 """
579 content = msg['content']
579 content = msg['content']
580 # If a SystemExit is passed along, this means exit() was called - also
580 # If a SystemExit is passed along, this means exit() was called - also
581 # all the ipython %exit magic syntax of '-k' to be used to keep
581 # all the ipython %exit magic syntax of '-k' to be used to keep
582 # the kernel running
582 # the kernel running
583 if content['ename']=='SystemExit':
583 if content['ename']=='SystemExit':
584 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
584 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
585 self._keep_kernel_on_exit = keepkernel
585 self._keep_kernel_on_exit = keepkernel
586 self.exit_requested.emit()
586 self.exit_requested.emit(self)
587 else:
587 else:
588 traceback = ''.join(content['traceback'])
588 traceback = ''.join(content['traceback'])
589 self._append_plain_text(traceback)
589 self._append_plain_text(traceback)
590
590
591 def _process_execute_ok(self, msg):
591 def _process_execute_ok(self, msg):
592 """ Process a reply for a successful execution equest.
592 """ Process a reply for a successful execution equest.
593 """
593 """
594 payload = msg['content']['payload']
594 payload = msg['content']['payload']
595 for item in payload:
595 for item in payload:
596 if not self._process_execute_payload(item):
596 if not self._process_execute_payload(item):
597 warning = 'Warning: received unknown payload of type %s'
597 warning = 'Warning: received unknown payload of type %s'
598 print(warning % repr(item['source']))
598 print(warning % repr(item['source']))
599
599
600 def _process_execute_payload(self, item):
600 def _process_execute_payload(self, item):
601 """ Process a single payload item from the list of payload items in an
601 """ Process a single payload item from the list of payload items in an
602 execution reply. Returns whether the payload was handled.
602 execution reply. Returns whether the payload was handled.
603 """
603 """
604 # The basic FrontendWidget doesn't handle payloads, as they are a
604 # The basic FrontendWidget doesn't handle payloads, as they are a
605 # mechanism for going beyond the standard Python interpreter model.
605 # mechanism for going beyond the standard Python interpreter model.
606 return False
606 return False
607
607
608 def _show_interpreter_prompt(self):
608 def _show_interpreter_prompt(self):
609 """ Shows a prompt for the interpreter.
609 """ Shows a prompt for the interpreter.
610 """
610 """
611 self._show_prompt('>>> ')
611 self._show_prompt('>>> ')
612
612
613 def _show_interpreter_prompt_for_reply(self, msg):
613 def _show_interpreter_prompt_for_reply(self, msg):
614 """ Shows a prompt for the interpreter given an 'execute_reply' message.
614 """ Shows a prompt for the interpreter given an 'execute_reply' message.
615 """
615 """
616 self._show_interpreter_prompt()
616 self._show_interpreter_prompt()
617
617
618 #------ Signal handlers ----------------------------------------------------
618 #------ Signal handlers ----------------------------------------------------
619
619
620 def _document_contents_change(self, position, removed, added):
620 def _document_contents_change(self, position, removed, added):
621 """ Called whenever the document's content changes. Display a call tip
621 """ Called whenever the document's content changes. Display a call tip
622 if appropriate.
622 if appropriate.
623 """
623 """
624 # Calculate where the cursor should be *after* the change:
624 # Calculate where the cursor should be *after* the change:
625 position += added
625 position += added
626
626
627 document = self._control.document()
627 document = self._control.document()
628 if position == self._get_cursor().position():
628 if position == self._get_cursor().position():
629 self._call_tip()
629 self._call_tip()
630
630
631 #------ Trait default initializers -----------------------------------------
631 #------ Trait default initializers -----------------------------------------
632
632
633 def _banner_default(self):
633 def _banner_default(self):
634 """ Returns the standard Python banner.
634 """ Returns the standard Python banner.
635 """
635 """
636 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
636 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
637 '"license" for more information.'
637 '"license" for more information.'
638 return banner % (sys.version, sys.platform)
638 return banner % (sys.version, sys.platform)
@@ -1,549 +1,549 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 import time
15 import time
16 from textwrap import dedent
16 from textwrap import dedent
17
17
18 # System library imports
18 # System library imports
19 from IPython.external.qt import QtCore, QtGui
19 from IPython.external.qt import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 transform_ipy_prompt
23 transform_ipy_prompt
24 from IPython.utils.traitlets import Bool, Unicode
24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 import styles
26 import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85 syntax_style = Unicode(config=True,
85 syntax_style = Unicode(config=True,
86 help="""
86 help="""
87 If not empty, use this Pygments style for syntax highlighting.
87 If not empty, use this Pygments style for syntax highlighting.
88 Otherwise, the style sheet is queried for Pygments style
88 Otherwise, the style sheet is queried for Pygments style
89 information.
89 information.
90 """)
90 """)
91
91
92 # Prompts.
92 # Prompts.
93 in_prompt = Unicode(default_in_prompt, config=True)
93 in_prompt = Unicode(default_in_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
98
98
99 # FrontendWidget protected class variables.
99 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
100 _input_splitter_class = IPythonInputSplitter
101
101
102 # IPythonWidget protected class variables.
102 # IPythonWidget protected class variables.
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_page = 'IPython.zmq.page.page'
107 _payload_source_page = 'IPython.zmq.page.page'
108 _retrying_history_request = False
108 _retrying_history_request = False
109
109
110 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
111 # 'object' interface
111 # 'object' interface
112 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
113
113
114 def __init__(self, *args, **kw):
114 def __init__(self, *args, **kw):
115 super(IPythonWidget, self).__init__(*args, **kw)
115 super(IPythonWidget, self).__init__(*args, **kw)
116
116
117 # IPythonWidget protected variables.
117 # IPythonWidget protected variables.
118 self._payload_handlers = {
118 self._payload_handlers = {
119 self._payload_source_edit : self._handle_payload_edit,
119 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_exit : self._handle_payload_exit,
120 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_page : self._handle_payload_page,
121 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_next_input : self._handle_payload_next_input }
122 self._payload_source_next_input : self._handle_payload_next_input }
123 self._previous_prompt_obj = None
123 self._previous_prompt_obj = None
124 self._keep_kernel_on_exit = None
124 self._keep_kernel_on_exit = None
125
125
126 # Initialize widget styling.
126 # Initialize widget styling.
127 if self.style_sheet:
127 if self.style_sheet:
128 self._style_sheet_changed()
128 self._style_sheet_changed()
129 self._syntax_style_changed()
129 self._syntax_style_changed()
130 else:
130 else:
131 self.set_default_style()
131 self.set_default_style()
132
132
133 #---------------------------------------------------------------------------
133 #---------------------------------------------------------------------------
134 # 'BaseFrontendMixin' abstract interface
134 # 'BaseFrontendMixin' abstract interface
135 #---------------------------------------------------------------------------
135 #---------------------------------------------------------------------------
136
136
137 def _handle_complete_reply(self, rep):
137 def _handle_complete_reply(self, rep):
138 """ Reimplemented to support IPython's improved completion machinery.
138 """ Reimplemented to support IPython's improved completion machinery.
139 """
139 """
140 self.log.debug("complete: %s", rep.get('content', ''))
140 self.log.debug("complete: %s", rep.get('content', ''))
141 cursor = self._get_cursor()
141 cursor = self._get_cursor()
142 info = self._request_info.get('complete')
142 info = self._request_info.get('complete')
143 if info and info.id == rep['parent_header']['msg_id'] and \
143 if info and info.id == rep['parent_header']['msg_id'] and \
144 info.pos == cursor.position():
144 info.pos == cursor.position():
145 matches = rep['content']['matches']
145 matches = rep['content']['matches']
146 text = rep['content']['matched_text']
146 text = rep['content']['matched_text']
147 offset = len(text)
147 offset = len(text)
148
148
149 # Clean up matches with period and path separators if the matched
149 # Clean up matches with period and path separators if the matched
150 # text has not been transformed. This is done by truncating all
150 # text has not been transformed. This is done by truncating all
151 # but the last component and then suitably decreasing the offset
151 # but the last component and then suitably decreasing the offset
152 # between the current cursor position and the start of completion.
152 # between the current cursor position and the start of completion.
153 if len(matches) > 1 and matches[0][:offset] == text:
153 if len(matches) > 1 and matches[0][:offset] == text:
154 parts = re.split(r'[./\\]', text)
154 parts = re.split(r'[./\\]', text)
155 sep_count = len(parts) - 1
155 sep_count = len(parts) - 1
156 if sep_count:
156 if sep_count:
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 matches = [ match[chop_length:] for match in matches ]
158 matches = [ match[chop_length:] for match in matches ]
159 offset -= chop_length
159 offset -= chop_length
160
160
161 # Move the cursor to the start of the match and complete.
161 # Move the cursor to the start of the match and complete.
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 self._complete_with_items(cursor, matches)
163 self._complete_with_items(cursor, matches)
164
164
165 def _handle_execute_reply(self, msg):
165 def _handle_execute_reply(self, msg):
166 """ Reimplemented to support prompt requests.
166 """ Reimplemented to support prompt requests.
167 """
167 """
168 info = self._request_info.get('execute')
168 info = self._request_info.get('execute')
169 if info and info.id == msg['parent_header']['msg_id']:
169 if info and info.id == msg['parent_header']['msg_id']:
170 if info.kind == 'prompt':
170 if info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
171 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
172 self._show_interpreter_prompt(number)
173 else:
173 else:
174 super(IPythonWidget, self)._handle_execute_reply(msg)
174 super(IPythonWidget, self)._handle_execute_reply(msg)
175
175
176 def _handle_history_reply(self, msg):
176 def _handle_history_reply(self, msg):
177 """ Implemented to handle history tail replies, which are only supported
177 """ Implemented to handle history tail replies, which are only supported
178 by the IPython kernel.
178 by the IPython kernel.
179 """
179 """
180 self.log.debug("history: %s", msg.get('content', ''))
180 self.log.debug("history: %s", msg.get('content', ''))
181 content = msg['content']
181 content = msg['content']
182 if 'history' not in content:
182 if 'history' not in content:
183 self.log.error("History request failed: %r"%content)
183 self.log.error("History request failed: %r"%content)
184 if content.get('status', '') == 'aborted' and \
184 if content.get('status', '') == 'aborted' and \
185 not self._retrying_history_request:
185 not self._retrying_history_request:
186 # a *different* action caused this request to be aborted, so
186 # a *different* action caused this request to be aborted, so
187 # we should try again.
187 # we should try again.
188 self.log.error("Retrying aborted history request")
188 self.log.error("Retrying aborted history request")
189 # prevent multiple retries of aborted requests:
189 # prevent multiple retries of aborted requests:
190 self._retrying_history_request = True
190 self._retrying_history_request = True
191 # wait out the kernel's queue flush, which is currently timed at 0.1s
191 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 time.sleep(0.25)
192 time.sleep(0.25)
193 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
193 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 else:
194 else:
195 self._retrying_history_request = False
195 self._retrying_history_request = False
196 return
196 return
197 # reset retry flag
197 # reset retry flag
198 self._retrying_history_request = False
198 self._retrying_history_request = False
199 history_items = content['history']
199 history_items = content['history']
200 items = [ line.rstrip() for _, _, line in history_items ]
200 items = [ line.rstrip() for _, _, line in history_items ]
201 self._set_history(items)
201 self._set_history(items)
202
202
203 def _handle_pyout(self, msg):
203 def _handle_pyout(self, msg):
204 """ Reimplemented for IPython-style "display hook".
204 """ Reimplemented for IPython-style "display hook".
205 """
205 """
206 self.log.debug("pyout: %s", msg.get('content', ''))
206 self.log.debug("pyout: %s", msg.get('content', ''))
207 if not self._hidden and self._is_from_this_session(msg):
207 if not self._hidden and self._is_from_this_session(msg):
208 content = msg['content']
208 content = msg['content']
209 prompt_number = content['execution_count']
209 prompt_number = content['execution_count']
210 data = content['data']
210 data = content['data']
211 if data.has_key('text/html'):
211 if data.has_key('text/html'):
212 self._append_plain_text(self.output_sep, True)
212 self._append_plain_text(self.output_sep, True)
213 self._append_html(self._make_out_prompt(prompt_number), True)
213 self._append_html(self._make_out_prompt(prompt_number), True)
214 html = data['text/html']
214 html = data['text/html']
215 self._append_plain_text('\n', True)
215 self._append_plain_text('\n', True)
216 self._append_html(html + self.output_sep2, True)
216 self._append_html(html + self.output_sep2, True)
217 elif data.has_key('text/plain'):
217 elif data.has_key('text/plain'):
218 self._append_plain_text(self.output_sep, True)
218 self._append_plain_text(self.output_sep, True)
219 self._append_html(self._make_out_prompt(prompt_number), True)
219 self._append_html(self._make_out_prompt(prompt_number), True)
220 text = data['text/plain']
220 text = data['text/plain']
221 # If the repr is multiline, make sure we start on a new line,
221 # If the repr is multiline, make sure we start on a new line,
222 # so that its lines are aligned.
222 # so that its lines are aligned.
223 if "\n" in text and not self.output_sep.endswith("\n"):
223 if "\n" in text and not self.output_sep.endswith("\n"):
224 self._append_plain_text('\n', True)
224 self._append_plain_text('\n', True)
225 self._append_plain_text(text + self.output_sep2, True)
225 self._append_plain_text(text + self.output_sep2, True)
226
226
227 def _handle_display_data(self, msg):
227 def _handle_display_data(self, msg):
228 """ The base handler for the ``display_data`` message.
228 """ The base handler for the ``display_data`` message.
229 """
229 """
230 self.log.debug("display: %s", msg.get('content', ''))
230 self.log.debug("display: %s", msg.get('content', ''))
231 # For now, we don't display data from other frontends, but we
231 # For now, we don't display data from other frontends, but we
232 # eventually will as this allows all frontends to monitor the display
232 # eventually will as this allows all frontends to monitor the display
233 # data. But we need to figure out how to handle this in the GUI.
233 # data. But we need to figure out how to handle this in the GUI.
234 if not self._hidden and self._is_from_this_session(msg):
234 if not self._hidden and self._is_from_this_session(msg):
235 source = msg['content']['source']
235 source = msg['content']['source']
236 data = msg['content']['data']
236 data = msg['content']['data']
237 metadata = msg['content']['metadata']
237 metadata = msg['content']['metadata']
238 # In the regular IPythonWidget, we simply print the plain text
238 # In the regular IPythonWidget, we simply print the plain text
239 # representation.
239 # representation.
240 if data.has_key('text/html'):
240 if data.has_key('text/html'):
241 html = data['text/html']
241 html = data['text/html']
242 self._append_html(html, True)
242 self._append_html(html, True)
243 elif data.has_key('text/plain'):
243 elif data.has_key('text/plain'):
244 text = data['text/plain']
244 text = data['text/plain']
245 self._append_plain_text(text, True)
245 self._append_plain_text(text, True)
246 # This newline seems to be needed for text and html output.
246 # This newline seems to be needed for text and html output.
247 self._append_plain_text(u'\n', True)
247 self._append_plain_text(u'\n', True)
248
248
249 def _started_channels(self):
249 def _started_channels(self):
250 """ Reimplemented to make a history request.
250 """ Reimplemented to make a history request.
251 """
251 """
252 super(IPythonWidget, self)._started_channels()
252 super(IPythonWidget, self)._started_channels()
253 self.kernel_manager.shell_channel.history(hist_access_type='tail',
253 self.kernel_manager.shell_channel.history(hist_access_type='tail',
254 n=1000)
254 n=1000)
255 #---------------------------------------------------------------------------
255 #---------------------------------------------------------------------------
256 # 'ConsoleWidget' public interface
256 # 'ConsoleWidget' public interface
257 #---------------------------------------------------------------------------
257 #---------------------------------------------------------------------------
258
258
259 def copy(self):
259 def copy(self):
260 """ Copy the currently selected text to the clipboard, removing prompts
260 """ Copy the currently selected text to the clipboard, removing prompts
261 if possible.
261 if possible.
262 """
262 """
263 text = self._control.textCursor().selection().toPlainText()
263 text = self._control.textCursor().selection().toPlainText()
264 if text:
264 if text:
265 lines = map(transform_ipy_prompt, text.splitlines())
265 lines = map(transform_ipy_prompt, text.splitlines())
266 text = '\n'.join(lines)
266 text = '\n'.join(lines)
267 QtGui.QApplication.clipboard().setText(text)
267 QtGui.QApplication.clipboard().setText(text)
268
268
269 #---------------------------------------------------------------------------
269 #---------------------------------------------------------------------------
270 # 'FrontendWidget' public interface
270 # 'FrontendWidget' public interface
271 #---------------------------------------------------------------------------
271 #---------------------------------------------------------------------------
272
272
273 def execute_file(self, path, hidden=False):
273 def execute_file(self, path, hidden=False):
274 """ Reimplemented to use the 'run' magic.
274 """ Reimplemented to use the 'run' magic.
275 """
275 """
276 # Use forward slashes on Windows to avoid escaping each separator.
276 # Use forward slashes on Windows to avoid escaping each separator.
277 if sys.platform == 'win32':
277 if sys.platform == 'win32':
278 path = os.path.normpath(path).replace('\\', '/')
278 path = os.path.normpath(path).replace('\\', '/')
279
279
280 # Perhaps we should not be using %run directly, but while we
280 # Perhaps we should not be using %run directly, but while we
281 # are, it is necessary to quote filenames containing spaces or quotes.
281 # are, it is necessary to quote filenames containing spaces or quotes.
282 # Escaping quotes in filename in %run seems tricky and inconsistent,
282 # Escaping quotes in filename in %run seems tricky and inconsistent,
283 # so not trying it at present.
283 # so not trying it at present.
284 if '"' in path:
284 if '"' in path:
285 if "'" in path:
285 if "'" in path:
286 raise ValueError("Can't run filename containing both single "
286 raise ValueError("Can't run filename containing both single "
287 "and double quotes: %s" % path)
287 "and double quotes: %s" % path)
288 path = "'%s'" % path
288 path = "'%s'" % path
289 elif ' ' in path or "'" in path:
289 elif ' ' in path or "'" in path:
290 path = '"%s"' % path
290 path = '"%s"' % path
291
291
292 self.execute('%%run %s' % path, hidden=hidden)
292 self.execute('%%run %s' % path, hidden=hidden)
293
293
294 #---------------------------------------------------------------------------
294 #---------------------------------------------------------------------------
295 # 'FrontendWidget' protected interface
295 # 'FrontendWidget' protected interface
296 #---------------------------------------------------------------------------
296 #---------------------------------------------------------------------------
297
297
298 def _complete(self):
298 def _complete(self):
299 """ Reimplemented to support IPython's improved completion machinery.
299 """ Reimplemented to support IPython's improved completion machinery.
300 """
300 """
301 # We let the kernel split the input line, so we *always* send an empty
301 # We let the kernel split the input line, so we *always* send an empty
302 # text field. Readline-based frontends do get a real text field which
302 # text field. Readline-based frontends do get a real text field which
303 # they can use.
303 # they can use.
304 text = ''
304 text = ''
305
305
306 # Send the completion request to the kernel
306 # Send the completion request to the kernel
307 msg_id = self.kernel_manager.shell_channel.complete(
307 msg_id = self.kernel_manager.shell_channel.complete(
308 text, # text
308 text, # text
309 self._get_input_buffer_cursor_line(), # line
309 self._get_input_buffer_cursor_line(), # line
310 self._get_input_buffer_cursor_column(), # cursor_pos
310 self._get_input_buffer_cursor_column(), # cursor_pos
311 self.input_buffer) # block
311 self.input_buffer) # block
312 pos = self._get_cursor().position()
312 pos = self._get_cursor().position()
313 info = self._CompletionRequest(msg_id, pos)
313 info = self._CompletionRequest(msg_id, pos)
314 self._request_info['complete'] = info
314 self._request_info['complete'] = info
315
315
316 def _process_execute_error(self, msg):
316 def _process_execute_error(self, msg):
317 """ Reimplemented for IPython-style traceback formatting.
317 """ Reimplemented for IPython-style traceback formatting.
318 """
318 """
319 content = msg['content']
319 content = msg['content']
320 traceback = '\n'.join(content['traceback']) + '\n'
320 traceback = '\n'.join(content['traceback']) + '\n'
321 if False:
321 if False:
322 # FIXME: For now, tracebacks come as plain text, so we can't use
322 # FIXME: For now, tracebacks come as plain text, so we can't use
323 # the html renderer yet. Once we refactor ultratb to produce
323 # the html renderer yet. Once we refactor ultratb to produce
324 # properly styled tracebacks, this branch should be the default
324 # properly styled tracebacks, this branch should be the default
325 traceback = traceback.replace(' ', '&nbsp;')
325 traceback = traceback.replace(' ', '&nbsp;')
326 traceback = traceback.replace('\n', '<br/>')
326 traceback = traceback.replace('\n', '<br/>')
327
327
328 ename = content['ename']
328 ename = content['ename']
329 ename_styled = '<span class="error">%s</span>' % ename
329 ename_styled = '<span class="error">%s</span>' % ename
330 traceback = traceback.replace(ename, ename_styled)
330 traceback = traceback.replace(ename, ename_styled)
331
331
332 self._append_html(traceback)
332 self._append_html(traceback)
333 else:
333 else:
334 # This is the fallback for now, using plain text with ansi escapes
334 # This is the fallback for now, using plain text with ansi escapes
335 self._append_plain_text(traceback)
335 self._append_plain_text(traceback)
336
336
337 def _process_execute_payload(self, item):
337 def _process_execute_payload(self, item):
338 """ Reimplemented to dispatch payloads to handler methods.
338 """ Reimplemented to dispatch payloads to handler methods.
339 """
339 """
340 handler = self._payload_handlers.get(item['source'])
340 handler = self._payload_handlers.get(item['source'])
341 if handler is None:
341 if handler is None:
342 # We have no handler for this type of payload, simply ignore it
342 # We have no handler for this type of payload, simply ignore it
343 return False
343 return False
344 else:
344 else:
345 handler(item)
345 handler(item)
346 return True
346 return True
347
347
348 def _show_interpreter_prompt(self, number=None):
348 def _show_interpreter_prompt(self, number=None):
349 """ Reimplemented for IPython-style prompts.
349 """ Reimplemented for IPython-style prompts.
350 """
350 """
351 # If a number was not specified, make a prompt number request.
351 # If a number was not specified, make a prompt number request.
352 if number is None:
352 if number is None:
353 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
353 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
354 info = self._ExecutionRequest(msg_id, 'prompt')
354 info = self._ExecutionRequest(msg_id, 'prompt')
355 self._request_info['execute'] = info
355 self._request_info['execute'] = info
356 return
356 return
357
357
358 # Show a new prompt and save information about it so that it can be
358 # Show a new prompt and save information about it so that it can be
359 # updated later if the prompt number turns out to be wrong.
359 # updated later if the prompt number turns out to be wrong.
360 self._prompt_sep = self.input_sep
360 self._prompt_sep = self.input_sep
361 self._show_prompt(self._make_in_prompt(number), html=True)
361 self._show_prompt(self._make_in_prompt(number), html=True)
362 block = self._control.document().lastBlock()
362 block = self._control.document().lastBlock()
363 length = len(self._prompt)
363 length = len(self._prompt)
364 self._previous_prompt_obj = self._PromptBlock(block, length, number)
364 self._previous_prompt_obj = self._PromptBlock(block, length, number)
365
365
366 # Update continuation prompt to reflect (possibly) new prompt length.
366 # Update continuation prompt to reflect (possibly) new prompt length.
367 self._set_continuation_prompt(
367 self._set_continuation_prompt(
368 self._make_continuation_prompt(self._prompt), html=True)
368 self._make_continuation_prompt(self._prompt), html=True)
369
369
370 def _show_interpreter_prompt_for_reply(self, msg):
370 def _show_interpreter_prompt_for_reply(self, msg):
371 """ Reimplemented for IPython-style prompts.
371 """ Reimplemented for IPython-style prompts.
372 """
372 """
373 # Update the old prompt number if necessary.
373 # Update the old prompt number if necessary.
374 content = msg['content']
374 content = msg['content']
375 previous_prompt_number = content['execution_count']
375 previous_prompt_number = content['execution_count']
376 if self._previous_prompt_obj and \
376 if self._previous_prompt_obj and \
377 self._previous_prompt_obj.number != previous_prompt_number:
377 self._previous_prompt_obj.number != previous_prompt_number:
378 block = self._previous_prompt_obj.block
378 block = self._previous_prompt_obj.block
379
379
380 # Make sure the prompt block has not been erased.
380 # Make sure the prompt block has not been erased.
381 if block.isValid() and block.text():
381 if block.isValid() and block.text():
382
382
383 # Remove the old prompt and insert a new prompt.
383 # Remove the old prompt and insert a new prompt.
384 cursor = QtGui.QTextCursor(block)
384 cursor = QtGui.QTextCursor(block)
385 cursor.movePosition(QtGui.QTextCursor.Right,
385 cursor.movePosition(QtGui.QTextCursor.Right,
386 QtGui.QTextCursor.KeepAnchor,
386 QtGui.QTextCursor.KeepAnchor,
387 self._previous_prompt_obj.length)
387 self._previous_prompt_obj.length)
388 prompt = self._make_in_prompt(previous_prompt_number)
388 prompt = self._make_in_prompt(previous_prompt_number)
389 self._prompt = self._insert_html_fetching_plain_text(
389 self._prompt = self._insert_html_fetching_plain_text(
390 cursor, prompt)
390 cursor, prompt)
391
391
392 # When the HTML is inserted, Qt blows away the syntax
392 # When the HTML is inserted, Qt blows away the syntax
393 # highlighting for the line, so we need to rehighlight it.
393 # highlighting for the line, so we need to rehighlight it.
394 self._highlighter.rehighlightBlock(cursor.block())
394 self._highlighter.rehighlightBlock(cursor.block())
395
395
396 self._previous_prompt_obj = None
396 self._previous_prompt_obj = None
397
397
398 # Show a new prompt with the kernel's estimated prompt number.
398 # Show a new prompt with the kernel's estimated prompt number.
399 self._show_interpreter_prompt(previous_prompt_number + 1)
399 self._show_interpreter_prompt(previous_prompt_number + 1)
400
400
401 #---------------------------------------------------------------------------
401 #---------------------------------------------------------------------------
402 # 'IPythonWidget' interface
402 # 'IPythonWidget' interface
403 #---------------------------------------------------------------------------
403 #---------------------------------------------------------------------------
404
404
405 def set_default_style(self, colors='lightbg'):
405 def set_default_style(self, colors='lightbg'):
406 """ Sets the widget style to the class defaults.
406 """ Sets the widget style to the class defaults.
407
407
408 Parameters:
408 Parameters:
409 -----------
409 -----------
410 colors : str, optional (default lightbg)
410 colors : str, optional (default lightbg)
411 Whether to use the default IPython light background or dark
411 Whether to use the default IPython light background or dark
412 background or B&W style.
412 background or B&W style.
413 """
413 """
414 colors = colors.lower()
414 colors = colors.lower()
415 if colors=='lightbg':
415 if colors=='lightbg':
416 self.style_sheet = styles.default_light_style_sheet
416 self.style_sheet = styles.default_light_style_sheet
417 self.syntax_style = styles.default_light_syntax_style
417 self.syntax_style = styles.default_light_syntax_style
418 elif colors=='linux':
418 elif colors=='linux':
419 self.style_sheet = styles.default_dark_style_sheet
419 self.style_sheet = styles.default_dark_style_sheet
420 self.syntax_style = styles.default_dark_syntax_style
420 self.syntax_style = styles.default_dark_syntax_style
421 elif colors=='nocolor':
421 elif colors=='nocolor':
422 self.style_sheet = styles.default_bw_style_sheet
422 self.style_sheet = styles.default_bw_style_sheet
423 self.syntax_style = styles.default_bw_syntax_style
423 self.syntax_style = styles.default_bw_syntax_style
424 else:
424 else:
425 raise KeyError("No such color scheme: %s"%colors)
425 raise KeyError("No such color scheme: %s"%colors)
426
426
427 #---------------------------------------------------------------------------
427 #---------------------------------------------------------------------------
428 # 'IPythonWidget' protected interface
428 # 'IPythonWidget' protected interface
429 #---------------------------------------------------------------------------
429 #---------------------------------------------------------------------------
430
430
431 def _edit(self, filename, line=None):
431 def _edit(self, filename, line=None):
432 """ Opens a Python script for editing.
432 """ Opens a Python script for editing.
433
433
434 Parameters:
434 Parameters:
435 -----------
435 -----------
436 filename : str
436 filename : str
437 A path to a local system file.
437 A path to a local system file.
438
438
439 line : int, optional
439 line : int, optional
440 A line of interest in the file.
440 A line of interest in the file.
441 """
441 """
442 if self.custom_edit:
442 if self.custom_edit:
443 self.custom_edit_requested.emit(filename, line)
443 self.custom_edit_requested.emit(filename, line)
444 elif not self.editor:
444 elif not self.editor:
445 self._append_plain_text('No default editor available.\n'
445 self._append_plain_text('No default editor available.\n'
446 'Specify a GUI text editor in the `IPythonWidget.editor` '
446 'Specify a GUI text editor in the `IPythonWidget.editor` '
447 'configurable to enable the %edit magic')
447 'configurable to enable the %edit magic')
448 else:
448 else:
449 try:
449 try:
450 filename = '"%s"' % filename
450 filename = '"%s"' % filename
451 if line and self.editor_line:
451 if line and self.editor_line:
452 command = self.editor_line.format(filename=filename,
452 command = self.editor_line.format(filename=filename,
453 line=line)
453 line=line)
454 else:
454 else:
455 try:
455 try:
456 command = self.editor.format()
456 command = self.editor.format()
457 except KeyError:
457 except KeyError:
458 command = self.editor.format(filename=filename)
458 command = self.editor.format(filename=filename)
459 else:
459 else:
460 command += ' ' + filename
460 command += ' ' + filename
461 except KeyError:
461 except KeyError:
462 self._append_plain_text('Invalid editor command.\n')
462 self._append_plain_text('Invalid editor command.\n')
463 else:
463 else:
464 try:
464 try:
465 Popen(command, shell=True)
465 Popen(command, shell=True)
466 except OSError:
466 except OSError:
467 msg = 'Opening editor with command "%s" failed.\n'
467 msg = 'Opening editor with command "%s" failed.\n'
468 self._append_plain_text(msg % command)
468 self._append_plain_text(msg % command)
469
469
470 def _make_in_prompt(self, number):
470 def _make_in_prompt(self, number):
471 """ Given a prompt number, returns an HTML In prompt.
471 """ Given a prompt number, returns an HTML In prompt.
472 """
472 """
473 try:
473 try:
474 body = self.in_prompt % number
474 body = self.in_prompt % number
475 except TypeError:
475 except TypeError:
476 # allow in_prompt to leave out number, e.g. '>>> '
476 # allow in_prompt to leave out number, e.g. '>>> '
477 body = self.in_prompt
477 body = self.in_prompt
478 return '<span class="in-prompt">%s</span>' % body
478 return '<span class="in-prompt">%s</span>' % body
479
479
480 def _make_continuation_prompt(self, prompt):
480 def _make_continuation_prompt(self, prompt):
481 """ Given a plain text version of an In prompt, returns an HTML
481 """ Given a plain text version of an In prompt, returns an HTML
482 continuation prompt.
482 continuation prompt.
483 """
483 """
484 end_chars = '...: '
484 end_chars = '...: '
485 space_count = len(prompt.lstrip('\n')) - len(end_chars)
485 space_count = len(prompt.lstrip('\n')) - len(end_chars)
486 body = '&nbsp;' * space_count + end_chars
486 body = '&nbsp;' * space_count + end_chars
487 return '<span class="in-prompt">%s</span>' % body
487 return '<span class="in-prompt">%s</span>' % body
488
488
489 def _make_out_prompt(self, number):
489 def _make_out_prompt(self, number):
490 """ Given a prompt number, returns an HTML Out prompt.
490 """ Given a prompt number, returns an HTML Out prompt.
491 """
491 """
492 body = self.out_prompt % number
492 body = self.out_prompt % number
493 return '<span class="out-prompt">%s</span>' % body
493 return '<span class="out-prompt">%s</span>' % body
494
494
495 #------ Payload handlers --------------------------------------------------
495 #------ Payload handlers --------------------------------------------------
496
496
497 # Payload handlers with a generic interface: each takes the opaque payload
497 # Payload handlers with a generic interface: each takes the opaque payload
498 # dict, unpacks it and calls the underlying functions with the necessary
498 # dict, unpacks it and calls the underlying functions with the necessary
499 # arguments.
499 # arguments.
500
500
501 def _handle_payload_edit(self, item):
501 def _handle_payload_edit(self, item):
502 self._edit(item['filename'], item['line_number'])
502 self._edit(item['filename'], item['line_number'])
503
503
504 def _handle_payload_exit(self, item):
504 def _handle_payload_exit(self, item):
505 self._keep_kernel_on_exit = item['keepkernel']
505 self._keep_kernel_on_exit = item['keepkernel']
506 self.exit_requested.emit()
506 self.exit_requested.emit(self)
507
507
508 def _handle_payload_next_input(self, item):
508 def _handle_payload_next_input(self, item):
509 self.input_buffer = dedent(item['text'].rstrip())
509 self.input_buffer = dedent(item['text'].rstrip())
510
510
511 def _handle_payload_page(self, item):
511 def _handle_payload_page(self, item):
512 # Since the plain text widget supports only a very small subset of HTML
512 # Since the plain text widget supports only a very small subset of HTML
513 # and we have no control over the HTML source, we only page HTML
513 # and we have no control over the HTML source, we only page HTML
514 # payloads in the rich text widget.
514 # payloads in the rich text widget.
515 if item['html'] and self.kind == 'rich':
515 if item['html'] and self.kind == 'rich':
516 self._page(item['html'], html=True)
516 self._page(item['html'], html=True)
517 else:
517 else:
518 self._page(item['text'], html=False)
518 self._page(item['text'], html=False)
519
519
520 #------ Trait change handlers --------------------------------------------
520 #------ Trait change handlers --------------------------------------------
521
521
522 def _style_sheet_changed(self):
522 def _style_sheet_changed(self):
523 """ Set the style sheets of the underlying widgets.
523 """ Set the style sheets of the underlying widgets.
524 """
524 """
525 self.setStyleSheet(self.style_sheet)
525 self.setStyleSheet(self.style_sheet)
526 self._control.document().setDefaultStyleSheet(self.style_sheet)
526 self._control.document().setDefaultStyleSheet(self.style_sheet)
527 if self._page_control:
527 if self._page_control:
528 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
528 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
529
529
530 bg_color = self._control.palette().window().color()
530 bg_color = self._control.palette().window().color()
531 self._ansi_processor.set_background_color(bg_color)
531 self._ansi_processor.set_background_color(bg_color)
532
532
533
533
534 def _syntax_style_changed(self):
534 def _syntax_style_changed(self):
535 """ Set the style for the syntax highlighter.
535 """ Set the style for the syntax highlighter.
536 """
536 """
537 if self._highlighter is None:
537 if self._highlighter is None:
538 # ignore premature calls
538 # ignore premature calls
539 return
539 return
540 if self.syntax_style:
540 if self.syntax_style:
541 self._highlighter.set_style(self.syntax_style)
541 self._highlighter.set_style(self.syntax_style)
542 else:
542 else:
543 self._highlighter.set_style_sheet(self.style_sheet)
543 self._highlighter.set_style_sheet(self.style_sheet)
544
544
545 #------ Trait default initializers -----------------------------------------
545 #------ Trait default initializers -----------------------------------------
546
546
547 def _banner_default(self):
547 def _banner_default(self):
548 from IPython.core.usage import default_gui_banner
548 from IPython.core.usage import default_gui_banner
549 return default_gui_banner
549 return default_gui_banner
@@ -1,802 +1,953 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12
12
13 """
13 """
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib imports
19 # stdlib imports
20 import json
20 import json
21 import os
21 import os
22 import signal
22 import signal
23 import sys
23 import sys
24
24
25 # System library imports
25 # System library imports
26 from IPython.external.qt import QtGui,QtCore
26 from IPython.external.qt import QtGui,QtCore
27 from pygments.styles import get_all_styles
27 from pygments.styles import get_all_styles
28
28
29 # Local imports
29 # Local imports
30 from IPython.config.application import boolean_flag
30 from IPython.config.application import boolean_flag
31 from IPython.core.application import BaseIPythonApplication
31 from IPython.core.application import BaseIPythonApplication
32 from IPython.core.profiledir import ProfileDir
32 from IPython.core.profiledir import ProfileDir
33 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
33 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
37 from IPython.frontend.qt.console import styles
37 from IPython.frontend.qt.console import styles
38 from IPython.frontend.qt.kernelmanager import QtKernelManager
38 from IPython.frontend.qt.kernelmanager import QtKernelManager
39 from IPython.utils.path import filefind
39 from IPython.utils.path import filefind
40 from IPython.utils.py3compat import str_to_bytes
40 from IPython.utils.py3compat import str_to_bytes
41 from IPython.utils.traitlets import (
41 from IPython.utils.traitlets import (
42 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
42 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
43 )
43 )
44 from IPython.zmq.ipkernel import (
44 from IPython.zmq.ipkernel import (
45 flags as ipkernel_flags,
45 flags as ipkernel_flags,
46 aliases as ipkernel_aliases,
46 aliases as ipkernel_aliases,
47 IPKernelApp
47 IPKernelApp
48 )
48 )
49 from IPython.zmq.session import Session, default_secure
49 from IPython.zmq.session import Session, default_secure
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51
51
52 import application_rc
52 import application_rc
53
53
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55 # Network Constants
55 # Network Constants
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57
57
58 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
58 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
59
59
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61 # Globals
61 # Globals
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63
63
64 _examples = """
64 _examples = """
65 ipython qtconsole # start the qtconsole
65 ipython qtconsole # start the qtconsole
66 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
66 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
67 """
67 """
68
68
69 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
70 # Classes
70 # Classes
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72
72
73 class MainWindow(QtGui.QMainWindow):
73 class MainWindow(QtGui.QMainWindow):
74
74
75 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
76 # 'object' interface
76 # 'object' interface
77 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
78
78
79 def __init__(self, app, frontend, existing=False, may_close=True,
79 def __init__(self, app, frontend, existing=False, may_close=True,
80 confirm_exit=True):
80 confirm_exit=True):
81 """ Create a MainWindow for the specified FrontendWidget.
81 """ Create a MainWindow for the specified FrontendWidget.
82
82
83 The app is passed as an argument to allow for different
83 The app is passed as an argument to allow for different
84 closing behavior depending on whether we are the Kernel's parent.
84 closing behavior depending on whether we are the Kernel's parent.
85
85
86 If existing is True, then this Console does not own the Kernel.
86 If existing is True, then this Console does not own the Kernel.
87
87
88 If may_close is True, then this Console is permitted to close the kernel
88 If may_close is True, then this Console is permitted to close the kernel
89 """
89 """
90 super(MainWindow, self).__init__()
90 super(MainWindow, self).__init__()
91 self._app = app
91 self._app = app
92 self._frontend = frontend
92 self._frontend = frontend
93 self._existing = existing
93 self._existing = existing
94 if existing:
94 if existing:
95 self._may_close = may_close
95 self._may_close = may_close
96 else:
96 else:
97 self._may_close = True
97 self._may_close = True
98 self._frontend.exit_requested.connect(self.close)
98 self._frontend.exit_requested.connect(self.close)
99 self._confirm_exit = confirm_exit
99 self._confirm_exit = confirm_exit
100 self.setCentralWidget(frontend)
101
100
101 self.tabWidget = QtGui.QTabWidget(self)
102 self.tabWidget.setDocumentMode(True)
103 self.tabWidget.setTabsClosable(True)
104 self.tabWidget.addTab(frontend,"QtConsole1")
105 self.tabWidget.tabCloseRequested[int].connect(self.closetab)
106
107 self.setCentralWidget(self.tabWidget)
108 self.updateTabBarVisibility()
109
110 def updateTabBarVisibility(self):
111 """ update visibility of the tabBar depending of the number of tab
112
113 0 or 1 tab, tabBar hiddent
114 2+ tabs, tabbarHidden
115
116 need to be called explicitely, or be connected to tabInserted/tabRemoved
117 """
118 if self.tabWidget.count() <= 1:
119 self.tabWidget.tabBar().setVisible(False)
120 else:
121 self.tabWidget.tabBar().setVisible(True)
122
123 def activeFrontend(self):
124 return self.tabWidget.currentWidget()
125
126 def closetab(self,tab):
127 """ Called when a user try to close a tab
128
129 It takes the number of the tab to be closed as argument, (does not for
130 now, but should) take care of whether or not shuting down the kernel
131 attached to the frontend
132 """
133 print "closing tab",tab
134 try:
135 if self.tabWidget.widget(tab)._local_kernel:
136 kernel_manager = self.tabWidget.widget(tab).kernel_manager.shutdown_kernel()
137 else:
138 print "not owning the kernel"
139 except:
140 print "can't ask the kernel to shutdown"
141 #if self.tabWidget.count() == 1:
142 #self.close()
143 self.tabWidget.removeTab(tab)
144 self.updateTabBarVisibility()
145
146 def addTabWithFrontend(self,frontend,name=None):
147 """ insert a tab with a given frontend in the tab bar, and give it a name
148
149 """
150 if not name:
151 name=str('no Name '+str(self.tabWidget.count()))
152 self.tabWidget.addTab(frontend,name)
153 self.updateTabBarVisibility()
154 frontend.exit_requested.connect(self.irequest)
155
156 def irequest(self,obj):
157 print "I request to exit",obj
158 print "which is tab:",self.tabWidget.indexOf(obj)
159 self.closetab(self.tabWidget.indexOf(obj))
102 # MenuBar is always present on Mac Os, so let's populate it with possible
160 # MenuBar is always present on Mac Os, so let's populate it with possible
103 # action, don't do it on other platform as some user might not want the
161 # action, don't do it on other platform as some user might not want the
104 # menu bar, or give them an option to remove it
162 # menu bar, or give them an option to remove it
105
163
106 def initMenuBar(self):
164 def initMenuBar(self):
107 #create menu in the order they should appear in the menu bar
165 #create menu in the order they should appear in the menu bar
108 self.fileMenu = self.menuBar().addMenu("File")
166 self.fileMenu = self.menuBar().addMenu("File")
109 self.editMenu = self.menuBar().addMenu("Edit")
167 self.editMenu = self.menuBar().addMenu("Edit")
110 self.fontMenu = self.menuBar().addMenu("Font")
168 self.fontMenu = self.menuBar().addMenu("Font")
111 self.windowMenu = self.menuBar().addMenu("Window")
169 self.windowMenu = self.menuBar().addMenu("Window")
112 self.magicMenu = self.menuBar().addMenu("Magic")
170 self.magicMenu = self.menuBar().addMenu("Magic")
113
171
114 # please keep the Help menu in Mac Os even if empty. It will
172 # please keep the Help menu in Mac Os even if empty. It will
115 # automatically contain a search field to search inside menus and
173 # automatically contain a search field to search inside menus and
116 # please keep it spelled in English, as long as Qt Doesn't support
174 # please keep it spelled in English, as long as Qt Doesn't support
117 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
175 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
118 # this search field fonctionnality
176 # this search field fonctionnality
119
177
120 self.helpMenu = self.menuBar().addMenu("Help")
178 self.helpMenu = self.menuBar().addMenu("Help")
121
179
122 # sould wrap every line of the following block into a try/except,
180 # sould wrap every line of the following block into a try/except,
123 # as we are not sure of instanciating a _frontend which support all
181 # as we are not sure of instanciating a _frontend which support all
124 # theses actions, but there might be a better way
182 # theses actions, but there might be a better way
125 try:
183 try:
126 self.fileMenu.addAction(self._frontend.print_action)
184 pass
185 self.print_action = QtGui.QAction("Print",
186 self,
187 shortcut="Ctrl+P",
188 triggered=self.undo_active_frontend)
189 self.fileMenu.addAction(self.print_action)
127 except AttributeError:
190 except AttributeError:
128 print "trying to add unexisting action, skipping"
191 print "trying to add unexisting action (print), skipping"
129
192
130 try:
193 try:
131 self.fileMenu.addAction(self._frontend.export_action)
194 self.export_action=QtGui.QAction("Export",
195 self,
196 shortcut="Ctrl+S",
197 triggered=self.export_action_active_frontend
198 )
199 self.fileMenu.addAction(self.export_action)
132 except AttributeError:
200 except AttributeError:
133 print "trying to add unexisting action, skipping"
201 print "trying to add unexisting action (Export), skipping"
134
202
135 try:
203 try:
136 self.fileMenu.addAction(self._frontend.select_all_action)
204 self.fileMenu.addAction(self._frontend.select_all_action)
137 except AttributeError:
205 except AttributeError:
138 print "trying to add unexisting action, skipping"
206 print "trying to add unexisting action, skipping"
139
207
140 try:
208 try:
141 self.undo_action = QtGui.QAction("Undo",
209 self.undo_action = QtGui.QAction("Undo",
142 self,
210 self,
143 shortcut="Ctrl+Z",
211 shortcut="Ctrl+Z",
144 statusTip="Undo last action if possible",
212 statusTip="Undo last action if possible",
145 triggered=self._frontend.undo)
213 triggered=self.undo_active_frontend)
146
214
147 self.editMenu.addAction(self.undo_action)
215 self.editMenu.addAction(self.undo_action)
148 except AttributeError:
216 except AttributeError:
149 print "trying to add unexisting action, skipping"
217 print "trying to add unexisting action (undo), skipping"
150
218
151 try:
219 try:
152 self.redo_action = QtGui.QAction("Redo",
220 self.redo_action = QtGui.QAction("Redo",
153 self,
221 self,
154 shortcut="Ctrl+Shift+Z",
222 shortcut="Ctrl+Shift+Z",
155 statusTip="Redo last action if possible",
223 statusTip="Redo last action if possible",
156 triggered=self._frontend.redo)
224 triggered=self.redo_active_frontend)
157 self.editMenu.addAction(self.redo_action)
225 self.editMenu.addAction(self.redo_action)
158 except AttributeError:
226 except AttributeError:
159 print "trying to add unexisting action, skipping"
227 print "trying to add unexisting action(redo), skipping"
160
228
161 try:
229 try:
162 self.fontMenu.addAction(self._frontend.increase_font_size)
230 pass#self.fontMenu.addAction(self.increase_font_size_active_frontend)
163 except AttributeError:
231 except AttributeError:
164 print "trying to add unexisting action, skipping"
232 print "trying to add unexisting action (increase font size), skipping"
165
233
166 try:
234 try:
167 self.fontMenu.addAction(self._frontend.decrease_font_size)
235 pass#self.fontMenu.addAction(self.decrease_font_size_active_frontend)
168 except AttributeError:
236 except AttributeError:
169 print "trying to add unexisting action, skipping"
237 print "trying to add unexisting action (decrease font size), skipping"
170
238
171 try:
239 try:
172 self.fontMenu.addAction(self._frontend.reset_font_size)
240 pass#self.fontMenu.addAction(self.reset_font_size_active_frontend)
173 except AttributeError:
241 except AttributeError:
174 print "trying to add unexisting action, skipping"
242 print "trying to add unexisting action (reset font size), skipping"
175
243
176 try:
244 try:
177 self.reset_action = QtGui.QAction("Reset",
245 self.reset_action = QtGui.QAction("Reset",
178 self,
246 self,
179 statusTip="Clear all varible from workspace",
247 statusTip="Clear all varible from workspace",
180 triggered=self._frontend.reset_magic)
248 triggered=self.reset_magic_active_frontend)
181 self.magicMenu.addAction(self.reset_action)
249 self.magicMenu.addAction(self.reset_action)
182 except AttributeError:
250 except AttributeError:
183 print "trying to add unexisting action (reset), skipping"
251 print "trying to add unexisting action (reset), skipping"
184
252
185 try:
253 try:
186 self.history_action = QtGui.QAction("History",
254 self.history_action = QtGui.QAction("History",
187 self,
255 self,
188 statusTip="show command history",
256 statusTip="show command history",
189 triggered=self._frontend.history_magic)
257 triggered=self.history_magic_active_frontend)
190 self.magicMenu.addAction(self.history_action)
258 self.magicMenu.addAction(self.history_action)
191 except AttributeError:
259 except AttributeError:
192 print "trying to add unexisting action (history), skipping"
260 print "trying to add unexisting action (history), skipping"
193
261
194 try:
262 try:
195 self.save_action = QtGui.QAction("Export History ",
263 self.save_action = QtGui.QAction("Export History ",
196 self,
264 self,
197 statusTip="Export History as Python File",
265 statusTip="Export History as Python File",
198 triggered=self._frontend.save_magic)
266 triggered=self.save_magic_active_frontend)
199 self.magicMenu.addAction(self.save_action)
267 self.magicMenu.addAction(self.save_action)
200 except AttributeError:
268 except AttributeError:
201 print "trying to add unexisting action (save), skipping"
269 print "trying to add unexisting action (save), skipping"
202
270
203 try:
271 try:
204 self.clear_action = QtGui.QAction("Clear",
272 self.clear_action = QtGui.QAction("Clear",
205 self,
273 self,
206 statusTip="Clear the console",
274 statusTip="Clear the console",
207 triggered=self._frontend.clear_magic)
275 triggered=self.clear_magic_active_frontend)
208 self.magicMenu.addAction(self.clear_action)
276 self.magicMenu.addAction(self.clear_action)
209 except AttributeError:
277 except AttributeError:
210 print "trying to add unexisting action, skipping"
278 print "trying to add unexisting action, skipping"
211
279
212 try:
280 try:
213 self.who_action = QtGui.QAction("Who",
281 self.who_action = QtGui.QAction("Who",
214 self,
282 self,
215 statusTip="List interactive variable",
283 statusTip="List interactive variable",
216 triggered=self._frontend.who_magic)
284 triggered=self.who_magic_active_frontend)
217 self.magicMenu.addAction(self.who_action)
285 self.magicMenu.addAction(self.who_action)
218 except AttributeError:
286 except AttributeError:
219 print "trying to add unexisting action (who), skipping"
287 print "trying to add unexisting action (who), skipping"
220
288
221 try:
289 try:
222 self.who_ls_action = QtGui.QAction("Who ls",
290 self.who_ls_action = QtGui.QAction("Who ls",
223 self,
291 self,
224 statusTip="Return a list of interactive variable",
292 statusTip="Return a list of interactive variable",
225 triggered=self._frontend.who_ls_magic)
293 triggered=self.who_ls_magic_active_frontend)
226 self.magicMenu.addAction(self.who_ls_action)
294 self.magicMenu.addAction(self.who_ls_action)
227 except AttributeError:
295 except AttributeError:
228 print "trying to add unexisting action (who_ls), skipping"
296 print "trying to add unexisting action (who_ls), skipping"
229
297
230 try:
298 try:
231 self.whos_action = QtGui.QAction("Whos",
299 self.whos_action = QtGui.QAction("Whos",
232 self,
300 self,
233 statusTip="List interactive variable with detail",
301 statusTip="List interactive variable with detail",
234 triggered=self._frontend.whos_magic)
302 triggered=self.whos_magic_active_frontend)
235 self.magicMenu.addAction(self.whos_action)
303 self.magicMenu.addAction(self.whos_action)
236 except AttributeError:
304 except AttributeError:
237 print "trying to add unexisting action (whos), skipping"
305 print "trying to add unexisting action (whos), skipping"
238
306
239
307 def undo_active_frontend(self):
308 self.activeFrontend().undo()
309
310 def redo_active_frontend(self):
311 self.activeFrontend().redo()
312 def reset_magic_active_frontend(self):
313 self.activeFrontend().reset_magic()
314 def history_magic_active_frontend(self):
315 self.activeFrontend().history_magic()
316 def save_magic_active_frontend(self):
317 self.activeFrontend().save_magic()
318 def clear_magic_active_frontend(self):
319 self.activeFrontend().clear_magic()
320 def who_magic_active_frontend(self):
321 self.activeFrontend().who_magic()
322 def who_ls_magic_active_frontend(self):
323 self.activeFrontend().who_ls_magic()
324 def whos_magic_active_frontend(self):
325 self.activeFrontend().whos_magic()
326
327 def print_action_active_frontend(self):
328 self.activeFrontend().print_action()
329
330 def export_action_active_frontend(self):
331 self.activeFrontend().export_action()
332
333 def select_all_action_frontend(self):
334 self.activeFrontend().select_all_action()
335
336 def increase_font_size_active_frontend(self):
337 self.activeFrontend().increase_font_size()
338 def decrease_font_size_active_frontend(self):
339 self.activeFrontend().decrease_font_size()
340 def reset_font_size_active_frontend(self):
341 self.activeFrontend().reset_font_size()
240 #---------------------------------------------------------------------------
342 #---------------------------------------------------------------------------
241 # QWidget interface
343 # QWidget interface
242 #---------------------------------------------------------------------------
344 #---------------------------------------------------------------------------
243
345
244 def closeEvent(self, event):
346 def closeEvent(self, event):
245 """ Close the window and the kernel (if necessary).
347 """ Close the window and the kernel (if necessary).
246
348
247 This will prompt the user if they are finished with the kernel, and if
349 This will prompt the user if they are finished with the kernel, and if
248 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
350 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
249 it closes without prompt.
351 it closes without prompt.
250 """
352 """
251 keepkernel = None #Use the prompt by default
353 keepkernel = None #Use the prompt by default
252 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
354 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
253 keepkernel = self._frontend._keep_kernel_on_exit
355 keepkernel = self._frontend._keep_kernel_on_exit
254
356
255 kernel_manager = self._frontend.kernel_manager
357 kernel_manager = self._frontend.kernel_manager
256
358
257 if keepkernel is None and not self._confirm_exit:
359 if keepkernel is None and not self._confirm_exit:
258 # don't prompt, just terminate the kernel if we own it
360 # don't prompt, just terminate the kernel if we own it
259 # or leave it alone if we don't
361 # or leave it alone if we don't
260 keepkernel = not self._existing
362 keepkernel = not self._existing
261
363
262 if keepkernel is None: #show prompt
364 if keepkernel is None: #show prompt
263 if kernel_manager and kernel_manager.channels_running:
365 if kernel_manager and kernel_manager.channels_running:
264 title = self.window().windowTitle()
366 title = self.window().windowTitle()
265 cancel = QtGui.QMessageBox.Cancel
367 cancel = QtGui.QMessageBox.Cancel
266 okay = QtGui.QMessageBox.Ok
368 okay = QtGui.QMessageBox.Ok
267 if self._may_close:
369 if self._may_close:
268 msg = "You are closing this Console window."
370 msg = "You are closing this Console window."
269 info = "Would you like to quit the Kernel and all attached Consoles as well?"
371 info = "Would you like to quit the Kernel and all attached Consoles as well?"
270 justthis = QtGui.QPushButton("&No, just this Console", self)
372 justthis = QtGui.QPushButton("&No, just this Console", self)
271 justthis.setShortcut('N')
373 justthis.setShortcut('N')
272 closeall = QtGui.QPushButton("&Yes, quit everything", self)
374 closeall = QtGui.QPushButton("&Yes, quit everything", self)
273 closeall.setShortcut('Y')
375 closeall.setShortcut('Y')
274 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
376 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
275 title, msg)
377 title, msg)
276 box.setInformativeText(info)
378 box.setInformativeText(info)
277 box.addButton(cancel)
379 box.addButton(cancel)
278 box.addButton(justthis, QtGui.QMessageBox.NoRole)
380 box.addButton(justthis, QtGui.QMessageBox.NoRole)
279 box.addButton(closeall, QtGui.QMessageBox.YesRole)
381 box.addButton(closeall, QtGui.QMessageBox.YesRole)
280 box.setDefaultButton(closeall)
382 box.setDefaultButton(closeall)
281 box.setEscapeButton(cancel)
383 box.setEscapeButton(cancel)
282 pixmap = QtGui.QPixmap(':/icon/IPythonConsole.png')
384 pixmap = QtGui.QPixmap(':/icon/IPythonConsole.png')
283 scaledpixmap = pixmap.scaledToWidth(64,mode=QtCore.Qt.SmoothTransformation)
385 scaledpixmap = pixmap.scaledToWidth(64,mode=QtCore.Qt.SmoothTransformation)
284 box.setIconPixmap(scaledpixmap)
386 box.setIconPixmap(scaledpixmap)
285 reply = box.exec_()
387 reply = box.exec_()
286 if reply == 1: # close All
388 if reply == 1: # close All
287 kernel_manager.shutdown_kernel()
389 kernel_manager.shutdown_kernel()
288 #kernel_manager.stop_channels()
390 #kernel_manager.stop_channels()
289 event.accept()
391 event.accept()
290 elif reply == 0: # close Console
392 elif reply == 0: # close Console
291 if not self._existing:
393 if not self._existing:
292 # Have kernel: don't quit, just close the window
394 # Have kernel: don't quit, just close the window
293 self._app.setQuitOnLastWindowClosed(False)
395 self._app.setQuitOnLastWindowClosed(False)
294 self.deleteLater()
396 self.deleteLater()
295 event.accept()
397 event.accept()
296 else:
398 else:
297 event.ignore()
399 event.ignore()
298 else:
400 else:
299 reply = QtGui.QMessageBox.question(self, title,
401 reply = QtGui.QMessageBox.question(self, title,
300 "Are you sure you want to close this Console?"+
402 "Are you sure you want to close this Console?"+
301 "\nThe Kernel and other Consoles will remain active.",
403 "\nThe Kernel and other Consoles will remain active.",
302 okay|cancel,
404 okay|cancel,
303 defaultButton=okay
405 defaultButton=okay
304 )
406 )
305 if reply == okay:
407 if reply == okay:
306 event.accept()
408 event.accept()
307 else:
409 else:
308 event.ignore()
410 event.ignore()
309 elif keepkernel: #close console but leave kernel running (no prompt)
411 elif keepkernel: #close console but leave kernel running (no prompt)
310 if kernel_manager and kernel_manager.channels_running:
412 if kernel_manager and kernel_manager.channels_running:
311 if not self._existing:
413 if not self._existing:
312 # I have the kernel: don't quit, just close the window
414 # I have the kernel: don't quit, just close the window
313 self._app.setQuitOnLastWindowClosed(False)
415 self._app.setQuitOnLastWindowClosed(False)
314 event.accept()
416 event.accept()
315 else: #close console and kernel (no prompt)
417 else: #close console and kernel (no prompt)
316 if kernel_manager and kernel_manager.channels_running:
418 if kernel_manager and kernel_manager.channels_running:
317 kernel_manager.shutdown_kernel()
419 kernel_manager.shutdown_kernel()
318 event.accept()
420 event.accept()
319
421
320 #-----------------------------------------------------------------------------
422 #-----------------------------------------------------------------------------
321 # Aliases and Flags
423 # Aliases and Flags
322 #-----------------------------------------------------------------------------
424 #-----------------------------------------------------------------------------
323
425
324 flags = dict(ipkernel_flags)
426 flags = dict(ipkernel_flags)
325 qt_flags = {
427 qt_flags = {
326 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
428 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
327 "Connect to an existing kernel. If no argument specified, guess most recent"),
429 "Connect to an existing kernel. If no argument specified, guess most recent"),
328 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
430 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
329 "Use a pure Python kernel instead of an IPython kernel."),
431 "Use a pure Python kernel instead of an IPython kernel."),
330 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
432 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
331 "Disable rich text support."),
433 "Disable rich text support."),
332 }
434 }
333 qt_flags.update(boolean_flag(
435 qt_flags.update(boolean_flag(
334 'gui-completion', 'ConsoleWidget.gui_completion',
436 'gui-completion', 'ConsoleWidget.gui_completion',
335 "use a GUI widget for tab completion",
437 "use a GUI widget for tab completion",
336 "use plaintext output for completion"
438 "use plaintext output for completion"
337 ))
439 ))
338 qt_flags.update(boolean_flag(
440 qt_flags.update(boolean_flag(
339 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
441 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
340 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
442 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
341 to force a direct exit without any confirmation.
443 to force a direct exit without any confirmation.
342 """,
444 """,
343 """Don't prompt the user when exiting. This will terminate the kernel
445 """Don't prompt the user when exiting. This will terminate the kernel
344 if it is owned by the frontend, and leave it alive if it is external.
446 if it is owned by the frontend, and leave it alive if it is external.
345 """
447 """
346 ))
448 ))
347 flags.update(qt_flags)
449 flags.update(qt_flags)
348
450
349 aliases = dict(ipkernel_aliases)
451 aliases = dict(ipkernel_aliases)
350
452
351 qt_aliases = dict(
453 qt_aliases = dict(
352 hb = 'IPythonQtConsoleApp.hb_port',
454 hb = 'IPythonQtConsoleApp.hb_port',
353 shell = 'IPythonQtConsoleApp.shell_port',
455 shell = 'IPythonQtConsoleApp.shell_port',
354 iopub = 'IPythonQtConsoleApp.iopub_port',
456 iopub = 'IPythonQtConsoleApp.iopub_port',
355 stdin = 'IPythonQtConsoleApp.stdin_port',
457 stdin = 'IPythonQtConsoleApp.stdin_port',
356 ip = 'IPythonQtConsoleApp.ip',
458 ip = 'IPythonQtConsoleApp.ip',
357 existing = 'IPythonQtConsoleApp.existing',
459 existing = 'IPythonQtConsoleApp.existing',
358 f = 'IPythonQtConsoleApp.connection_file',
460 f = 'IPythonQtConsoleApp.connection_file',
359
461
360 style = 'IPythonWidget.syntax_style',
462 style = 'IPythonWidget.syntax_style',
361 stylesheet = 'IPythonQtConsoleApp.stylesheet',
463 stylesheet = 'IPythonQtConsoleApp.stylesheet',
362 colors = 'ZMQInteractiveShell.colors',
464 colors = 'ZMQInteractiveShell.colors',
363
465
364 editor = 'IPythonWidget.editor',
466 editor = 'IPythonWidget.editor',
365 paging = 'ConsoleWidget.paging',
467 paging = 'ConsoleWidget.paging',
366 ssh = 'IPythonQtConsoleApp.sshserver',
468 ssh = 'IPythonQtConsoleApp.sshserver',
367 )
469 )
368 aliases.update(qt_aliases)
470 aliases.update(qt_aliases)
369
471
370
472
371 #-----------------------------------------------------------------------------
473 #-----------------------------------------------------------------------------
372 # IPythonQtConsole
474 # IPythonQtConsole
373 #-----------------------------------------------------------------------------
475 #-----------------------------------------------------------------------------
374
476
375
477
376 class IPythonQtConsoleApp(BaseIPythonApplication):
478 class IPythonQtConsoleApp(BaseIPythonApplication):
377 name = 'ipython-qtconsole'
479 name = 'ipython-qtconsole'
378 default_config_file_name='ipython_config.py'
480 default_config_file_name='ipython_config.py'
379
481
380 description = """
482 description = """
381 The IPython QtConsole.
483 The IPython QtConsole.
382
484
383 This launches a Console-style application using Qt. It is not a full
485 This launches a Console-style application using Qt. It is not a full
384 console, in that launched terminal subprocesses will not be able to accept
486 console, in that launched terminal subprocesses will not be able to accept
385 input.
487 input.
386
488
387 The QtConsole supports various extra features beyond the Terminal IPython
489 The QtConsole supports various extra features beyond the Terminal IPython
388 shell, such as inline plotting with matplotlib, via:
490 shell, such as inline plotting with matplotlib, via:
389
491
390 ipython qtconsole --pylab=inline
492 ipython qtconsole --pylab=inline
391
493
392 as well as saving your session as HTML, and printing the output.
494 as well as saving your session as HTML, and printing the output.
393
495
394 """
496 """
395 examples = _examples
497 examples = _examples
396
498
397 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
499 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
398 flags = Dict(flags)
500 flags = Dict(flags)
399 aliases = Dict(aliases)
501 aliases = Dict(aliases)
400
502
401 kernel_argv = List(Unicode)
503 kernel_argv = List(Unicode)
402
504
403 # create requested profiles by default, if they don't exist:
505 # create requested profiles by default, if they don't exist:
404 auto_create = CBool(True)
506 auto_create = CBool(True)
405 # connection info:
507 # connection info:
406 ip = Unicode(LOCALHOST, config=True,
508 ip = Unicode(LOCALHOST, config=True,
407 help="""Set the kernel\'s IP address [default localhost].
509 help="""Set the kernel\'s IP address [default localhost].
408 If the IP address is something other than localhost, then
510 If the IP address is something other than localhost, then
409 Consoles on other machines will be able to connect
511 Consoles on other machines will be able to connect
410 to the Kernel, so be careful!"""
512 to the Kernel, so be careful!"""
411 )
513 )
412
514
413 sshserver = Unicode('', config=True,
515 sshserver = Unicode('', config=True,
414 help="""The SSH server to use to connect to the kernel.""")
516 help="""The SSH server to use to connect to the kernel.""")
415 sshkey = Unicode('', config=True,
517 sshkey = Unicode('', config=True,
416 help="""Path to the ssh key to use for logging in to the ssh server.""")
518 help="""Path to the ssh key to use for logging in to the ssh server.""")
417
519
418 hb_port = Int(0, config=True,
520 hb_port = Int(0, config=True,
419 help="set the heartbeat port [default: random]")
521 help="set the heartbeat port [default: random]")
420 shell_port = Int(0, config=True,
522 shell_port = Int(0, config=True,
421 help="set the shell (XREP) port [default: random]")
523 help="set the shell (XREP) port [default: random]")
422 iopub_port = Int(0, config=True,
524 iopub_port = Int(0, config=True,
423 help="set the iopub (PUB) port [default: random]")
525 help="set the iopub (PUB) port [default: random]")
424 stdin_port = Int(0, config=True,
526 stdin_port = Int(0, config=True,
425 help="set the stdin (XREQ) port [default: random]")
527 help="set the stdin (XREQ) port [default: random]")
426 connection_file = Unicode('', config=True,
528 connection_file = Unicode('', config=True,
427 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
529 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
428
530
429 This file will contain the IP, ports, and authentication key needed to connect
531 This file will contain the IP, ports, and authentication key needed to connect
430 clients to this kernel. By default, this file will be created in the security-dir
532 clients to this kernel. By default, this file will be created in the security-dir
431 of the current profile, but can be specified by absolute path.
533 of the current profile, but can be specified by absolute path.
432 """)
534 """)
433 def _connection_file_default(self):
535 def _connection_file_default(self):
434 return 'kernel-%i.json' % os.getpid()
536 return 'kernel-%i.json' % os.getpid()
435
537
436 existing = Unicode('', config=True,
538 existing = Unicode('', config=True,
437 help="""Connect to an already running kernel""")
539 help="""Connect to an already running kernel""")
438
540
439 stylesheet = Unicode('', config=True,
541 stylesheet = Unicode('', config=True,
440 help="path to a custom CSS stylesheet")
542 help="path to a custom CSS stylesheet")
441
543
442 pure = CBool(False, config=True,
544 pure = CBool(False, config=True,
443 help="Use a pure Python kernel instead of an IPython kernel.")
545 help="Use a pure Python kernel instead of an IPython kernel.")
444 plain = CBool(False, config=True,
546 plain = CBool(False, config=True,
445 help="Use a plaintext widget instead of rich text (plain can't print/save).")
547 help="Use a plaintext widget instead of rich text (plain can't print/save).")
446
548
447 def _pure_changed(self, name, old, new):
549 def _pure_changed(self, name, old, new):
448 kind = 'plain' if self.plain else 'rich'
550 kind = 'plain' if self.plain else 'rich'
449 self.config.ConsoleWidget.kind = kind
551 self.config.ConsoleWidget.kind = kind
450 if self.pure:
552 if self.pure:
451 self.widget_factory = FrontendWidget
553 self.widget_factory = FrontendWidget
452 elif self.plain:
554 elif self.plain:
453 self.widget_factory = IPythonWidget
555 self.widget_factory = IPythonWidget
454 else:
556 else:
455 self.widget_factory = RichIPythonWidget
557 self.widget_factory = RichIPythonWidget
456
558
457 _plain_changed = _pure_changed
559 _plain_changed = _pure_changed
458
560
459 confirm_exit = CBool(True, config=True,
561 confirm_exit = CBool(True, config=True,
460 help="""
562 help="""
461 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
563 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
462 to force a direct exit without any confirmation.""",
564 to force a direct exit without any confirmation.""",
463 )
565 )
464
566
465 # the factory for creating a widget
567 # the factory for creating a widget
466 widget_factory = Any(RichIPythonWidget)
568 widget_factory = Any(RichIPythonWidget)
467
569
468 def parse_command_line(self, argv=None):
570 def parse_command_line(self, argv=None):
469 super(IPythonQtConsoleApp, self).parse_command_line(argv)
571 super(IPythonQtConsoleApp, self).parse_command_line(argv)
470 if argv is None:
572 if argv is None:
471 argv = sys.argv[1:]
573 argv = sys.argv[1:]
472
574
473 self.kernel_argv = list(argv) # copy
575 self.kernel_argv = list(argv) # copy
474 # kernel should inherit default config file from frontend
576 # kernel should inherit default config file from frontend
475 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
577 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
476 # Scrub frontend-specific flags
578 # Scrub frontend-specific flags
477 for a in argv:
579 for a in argv:
478 if a.startswith('-') and a.lstrip('-') in qt_flags:
580 if a.startswith('-') and a.lstrip('-') in qt_flags:
479 self.kernel_argv.remove(a)
581 self.kernel_argv.remove(a)
480 swallow_next = False
582 swallow_next = False
481 for a in argv:
583 for a in argv:
482 if swallow_next:
584 if swallow_next:
483 self.kernel_argv.remove(a)
585 self.kernel_argv.remove(a)
484 swallow_next = False
586 swallow_next = False
485 continue
587 continue
486 if a.startswith('-'):
588 if a.startswith('-'):
487 split = a.lstrip('-').split('=')
589 split = a.lstrip('-').split('=')
488 alias = split[0]
590 alias = split[0]
489 if alias in qt_aliases:
591 if alias in qt_aliases:
490 self.kernel_argv.remove(a)
592 self.kernel_argv.remove(a)
491 if len(split) == 1:
593 if len(split) == 1:
492 # alias passed with arg via space
594 # alias passed with arg via space
493 swallow_next = True
595 swallow_next = True
494
596
495 def init_connection_file(self):
597 def init_connection_file(self):
496 """find the connection file, and load the info if found.
598 """find the connection file, and load the info if found.
497
599
498 The current working directory and the current profile's security
600 The current working directory and the current profile's security
499 directory will be searched for the file if it is not given by
601 directory will be searched for the file if it is not given by
500 absolute path.
602 absolute path.
501
603
502 When attempting to connect to an existing kernel and the `--existing`
604 When attempting to connect to an existing kernel and the `--existing`
503 argument does not match an existing file, it will be interpreted as a
605 argument does not match an existing file, it will be interpreted as a
504 fileglob, and the matching file in the current profile's security dir
606 fileglob, and the matching file in the current profile's security dir
505 with the latest access time will be used.
607 with the latest access time will be used.
506 """
608 """
507 if self.existing:
609 if self.existing:
508 try:
610 try:
509 cf = find_connection_file(self.existing)
611 cf = find_connection_file(self.existing)
510 except Exception:
612 except Exception:
511 self.log.critical("Could not find existing kernel connection file %s", self.existing)
613 self.log.critical("Could not find existing kernel connection file %s", self.existing)
512 self.exit(1)
614 self.exit(1)
513 self.log.info("Connecting to existing kernel: %s" % cf)
615 self.log.info("Connecting to existing kernel: %s" % cf)
514 self.connection_file = cf
616 self.connection_file = cf
515 # should load_connection_file only be used for existing?
617 # should load_connection_file only be used for existing?
516 # as it is now, this allows reusing ports if an existing
618 # as it is now, this allows reusing ports if an existing
517 # file is requested
619 # file is requested
518 try:
620 try:
519 self.load_connection_file()
621 self.load_connection_file()
520 except Exception:
622 except Exception:
521 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
623 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
522 self.exit(1)
624 self.exit(1)
523
625
524 def load_connection_file(self):
626 def load_connection_file(self):
525 """load ip/port/hmac config from JSON connection file"""
627 """load ip/port/hmac config from JSON connection file"""
526 # this is identical to KernelApp.load_connection_file
628 # this is identical to KernelApp.load_connection_file
527 # perhaps it can be centralized somewhere?
629 # perhaps it can be centralized somewhere?
528 try:
630 try:
529 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
631 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
530 except IOError:
632 except IOError:
531 self.log.debug("Connection File not found: %s", self.connection_file)
633 self.log.debug("Connection File not found: %s", self.connection_file)
532 return
634 return
533 self.log.debug(u"Loading connection file %s", fname)
635 self.log.debug(u"Loading connection file %s", fname)
534 with open(fname) as f:
636 with open(fname) as f:
535 s = f.read()
637 s = f.read()
536 cfg = json.loads(s)
638 cfg = json.loads(s)
537 if self.ip == LOCALHOST and 'ip' in cfg:
639 if self.ip == LOCALHOST and 'ip' in cfg:
538 # not overridden by config or cl_args
640 # not overridden by config or cl_args
539 self.ip = cfg['ip']
641 self.ip = cfg['ip']
540 for channel in ('hb', 'shell', 'iopub', 'stdin'):
642 for channel in ('hb', 'shell', 'iopub', 'stdin'):
541 name = channel + '_port'
643 name = channel + '_port'
542 if getattr(self, name) == 0 and name in cfg:
644 if getattr(self, name) == 0 and name in cfg:
543 # not overridden by config or cl_args
645 # not overridden by config or cl_args
544 setattr(self, name, cfg[name])
646 setattr(self, name, cfg[name])
545 if 'key' in cfg:
647 if 'key' in cfg:
546 self.config.Session.key = str_to_bytes(cfg['key'])
648 self.config.Session.key = str_to_bytes(cfg['key'])
547
649
548 def init_ssh(self):
650 def init_ssh(self):
549 """set up ssh tunnels, if needed."""
651 """set up ssh tunnels, if needed."""
550 if not self.sshserver and not self.sshkey:
652 if not self.sshserver and not self.sshkey:
551 return
653 return
552
654
553 if self.sshkey and not self.sshserver:
655 if self.sshkey and not self.sshserver:
554 # specifying just the key implies that we are connecting directly
656 # specifying just the key implies that we are connecting directly
555 self.sshserver = self.ip
657 self.sshserver = self.ip
556 self.ip = LOCALHOST
658 self.ip = LOCALHOST
557
659
558 # build connection dict for tunnels:
660 # build connection dict for tunnels:
559 info = dict(ip=self.ip,
661 info = dict(ip=self.ip,
560 shell_port=self.shell_port,
662 shell_port=self.shell_port,
561 iopub_port=self.iopub_port,
663 iopub_port=self.iopub_port,
562 stdin_port=self.stdin_port,
664 stdin_port=self.stdin_port,
563 hb_port=self.hb_port
665 hb_port=self.hb_port
564 )
666 )
565
667
566 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
668 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
567
669
568 # tunnels return a new set of ports, which will be on localhost:
670 # tunnels return a new set of ports, which will be on localhost:
569 self.ip = LOCALHOST
671 self.ip = LOCALHOST
570 try:
672 try:
571 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
673 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
572 except:
674 except:
573 # even catch KeyboardInterrupt
675 # even catch KeyboardInterrupt
574 self.log.error("Could not setup tunnels", exc_info=True)
676 self.log.error("Could not setup tunnels", exc_info=True)
575 self.exit(1)
677 self.exit(1)
576
678
577 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
679 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
578
680
579 cf = self.connection_file
681 cf = self.connection_file
580 base,ext = os.path.splitext(cf)
682 base,ext = os.path.splitext(cf)
581 base = os.path.basename(base)
683 base = os.path.basename(base)
582 self.connection_file = os.path.basename(base)+'-ssh'+ext
684 self.connection_file = os.path.basename(base)+'-ssh'+ext
583 self.log.critical("To connect another client via this tunnel, use:")
685 self.log.critical("To connect another client via this tunnel, use:")
584 self.log.critical("--existing %s" % self.connection_file)
686 self.log.critical("--existing %s" % self.connection_file)
585
687
586 def init_kernel_manager(self):
688 def init_kernel_manager(self):
587 # Don't let Qt or ZMQ swallow KeyboardInterupts.
689 # Don't let Qt or ZMQ swallow KeyboardInterupts.
588 signal.signal(signal.SIGINT, signal.SIG_DFL)
690 signal.signal(signal.SIGINT, signal.SIG_DFL)
589 sec = self.profile_dir.security_dir
691 sec = self.profile_dir.security_dir
590 try:
692 try:
591 cf = filefind(self.connection_file, ['.', sec])
693 cf = filefind(self.connection_file, ['.', sec])
592 except IOError:
694 except IOError:
593 # file might not exist
695 # file might not exist
594 if self.connection_file == os.path.basename(self.connection_file):
696 if self.connection_file == os.path.basename(self.connection_file):
595 # just shortname, put it in security dir
697 # just shortname, put it in security dir
596 cf = os.path.join(sec, self.connection_file)
698 cf = os.path.join(sec, self.connection_file)
597 else:
699 else:
598 cf = self.connection_file
700 cf = self.connection_file
599
701
600 # Create a KernelManager and start a kernel.
702 # Create a KernelManager and start a kernel.
601 self.kernel_manager = QtKernelManager(
703 self.kernel_manager = QtKernelManager(
602 ip=self.ip,
704 ip=self.ip,
603 shell_port=self.shell_port,
705 shell_port=self.shell_port,
604 iopub_port=self.iopub_port,
706 iopub_port=self.iopub_port,
605 stdin_port=self.stdin_port,
707 stdin_port=self.stdin_port,
606 hb_port=self.hb_port,
708 hb_port=self.hb_port,
607 connection_file=cf,
709 connection_file=cf,
608 config=self.config,
710 config=self.config,
609 )
711 )
610 # start the kernel
712 # start the kernel
611 if not self.existing:
713 if not self.existing:
612 kwargs = dict(ipython=not self.pure)
714 kwargs = dict(ipython=not self.pure)
613 kwargs['extra_arguments'] = self.kernel_argv
715 kwargs['extra_arguments'] = self.kernel_argv
614 self.kernel_manager.start_kernel(**kwargs)
716 self.kernel_manager.start_kernel(**kwargs)
615 elif self.sshserver:
717 elif self.sshserver:
616 # ssh, write new connection file
718 # ssh, write new connection file
617 self.kernel_manager.write_connection_file()
719 self.kernel_manager.write_connection_file()
618 self.kernel_manager.start_channels()
720 self.kernel_manager.start_channels()
619
721
722 def createTabWithNewFrontend(self):
723 kernel_manager = QtKernelManager(
724 shell_address=(self.ip, self.shell_port),
725 sub_address=(self.ip, self.iopub_port),
726 stdin_address=(self.ip, self.stdin_port),
727 hb_address=(self.ip, self.hb_port),
728 config=self.config
729 )
730 # start the kernel
731 if not self.existing:
732 kwargs = dict(ip=self.ip, ipython=not self.pure)
733 kwargs['extra_arguments'] = self.kernel_argv
734 kernel_manager.start_kernel(**kwargs)
735 kernel_manager.start_channels()
736 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
737 widget = self.widget_factory(config=self.config,
738 local_kernel=local_kernel)
739 widget.kernel_manager = kernel_manager
740 self.window.addTabWithFrontend(widget)
741
742 def createTabAttachedToCurrentTabKernel(self):
743 currentWidget=self.window.tabWidget.currentWidget()
744 currentWidgetIndex=self.window.tabWidget.indexOf(currentWidget)
745 ckm=currentWidget.kernel_manager;
746 cwname=self.window.tabWidget.tabText(currentWidgetIndex);
747 kernel_manager = QtKernelManager(
748 shell_address=ckm.shell_address,
749 sub_address=ckm.sub_address,
750 stdin_address=ckm.stdin_address,
751 hb_address=ckm.hb_address,
752 config=self.config
753 )
754 kernel_manager.start_channels()
755 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
756 widget = self.widget_factory(config=self.config,
757 local_kernel=False)
758 widget.kernel_manager = kernel_manager
759 self.window.addTabWithFrontend(widget,name=str('('+cwname+') slave'))
620
760
621 def init_qt_elements(self):
761 def init_qt_elements(self):
622 # Create the widget.
762 # Create the widget.
623 self.app = QtGui.QApplication([])
763 self.app = QtGui.QApplication([])
624 pixmap=QtGui.QPixmap(':/icon/IPythonConsole.png')
764 pixmap=QtGui.QPixmap(':/icon/IPythonConsole.png')
625 icon=QtGui.QIcon(pixmap)
765 icon=QtGui.QIcon(pixmap)
626 QtGui.QApplication.setWindowIcon(icon)
766 QtGui.QApplication.setWindowIcon(icon)
627
767
628 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
768 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
629 self.widget = self.widget_factory(config=self.config,
769 self.widget = self.widget_factory(config=self.config,
630 local_kernel=local_kernel)
770 local_kernel=local_kernel)
631 self.widget.kernel_manager = self.kernel_manager
771 self.widget.kernel_manager = self.kernel_manager
632 self.window = MainWindow(self.app, self.widget, self.existing,
772 self.window = MainWindow(self.app, self.widget, self.existing,
633 may_close=local_kernel,
773 may_close=local_kernel,
634 confirm_exit=self.confirm_exit)
774 confirm_exit=self.confirm_exit)
635 self.window.initMenuBar()
775 self.window.initMenuBar()
636 self.window.setWindowTitle('Python' if self.pure else 'IPython')
776 self.window.setWindowTitle('Python' if self.pure else 'IPython')
637
777
638 def init_colors(self):
778 def init_colors(self):
639 """Configure the coloring of the widget"""
779 """Configure the coloring of the widget"""
640 # Note: This will be dramatically simplified when colors
780 # Note: This will be dramatically simplified when colors
641 # are removed from the backend.
781 # are removed from the backend.
642
782
643 if self.pure:
783 if self.pure:
644 # only IPythonWidget supports styling
784 # only IPythonWidget supports styling
645 return
785 return
646
786
647 # parse the colors arg down to current known labels
787 # parse the colors arg down to current known labels
648 try:
788 try:
649 colors = self.config.ZMQInteractiveShell.colors
789 colors = self.config.ZMQInteractiveShell.colors
650 except AttributeError:
790 except AttributeError:
651 colors = None
791 colors = None
652 try:
792 try:
653 style = self.config.IPythonWidget.colors
793 style = self.config.IPythonWidget.colors
654 except AttributeError:
794 except AttributeError:
655 style = None
795 style = None
656
796
657 # find the value for colors:
797 # find the value for colors:
658 if colors:
798 if colors:
659 colors=colors.lower()
799 colors=colors.lower()
660 if colors in ('lightbg', 'light'):
800 if colors in ('lightbg', 'light'):
661 colors='lightbg'
801 colors='lightbg'
662 elif colors in ('dark', 'linux'):
802 elif colors in ('dark', 'linux'):
663 colors='linux'
803 colors='linux'
664 else:
804 else:
665 colors='nocolor'
805 colors='nocolor'
666 elif style:
806 elif style:
667 if style=='bw':
807 if style=='bw':
668 colors='nocolor'
808 colors='nocolor'
669 elif styles.dark_style(style):
809 elif styles.dark_style(style):
670 colors='linux'
810 colors='linux'
671 else:
811 else:
672 colors='lightbg'
812 colors='lightbg'
673 else:
813 else:
674 colors=None
814 colors=None
675
815
676 # Configure the style.
816 # Configure the style.
677 widget = self.widget
817 widget = self.widget
678 if style:
818 if style:
679 widget.style_sheet = styles.sheet_from_template(style, colors)
819 widget.style_sheet = styles.sheet_from_template(style, colors)
680 widget.syntax_style = style
820 widget.syntax_style = style
681 widget._syntax_style_changed()
821 widget._syntax_style_changed()
682 widget._style_sheet_changed()
822 widget._style_sheet_changed()
683 elif colors:
823 elif colors:
684 # use a default style
824 # use a default style
685 widget.set_default_style(colors=colors)
825 widget.set_default_style(colors=colors)
686 else:
826 else:
687 # this is redundant for now, but allows the widget's
827 # this is redundant for now, but allows the widget's
688 # defaults to change
828 # defaults to change
689 widget.set_default_style()
829 widget.set_default_style()
690
830
691 if self.stylesheet:
831 if self.stylesheet:
692 # we got an expicit stylesheet
832 # we got an expicit stylesheet
693 if os.path.isfile(self.stylesheet):
833 if os.path.isfile(self.stylesheet):
694 with open(self.stylesheet) as f:
834 with open(self.stylesheet) as f:
695 sheet = f.read()
835 sheet = f.read()
696 widget.style_sheet = sheet
836 widget.style_sheet = sheet
697 widget._style_sheet_changed()
837 widget._style_sheet_changed()
698 else:
838 else:
699 raise IOError("Stylesheet %r not found."%self.stylesheet)
839 raise IOError("Stylesheet %r not found."%self.stylesheet)
700
840
701 def initialize(self, argv=None):
841 def initialize(self, argv=None):
702 super(IPythonQtConsoleApp, self).initialize(argv)
842 super(IPythonQtConsoleApp, self).initialize(argv)
703 self.init_connection_file()
843 self.init_connection_file()
704 default_secure(self.config)
844 default_secure(self.config)
705 self.init_ssh()
845 self.init_ssh()
706 self.init_kernel_manager()
846 self.init_kernel_manager()
707 self.init_qt_elements()
847 self.init_qt_elements()
708 self.init_colors()
848 self.init_colors()
709 self.init_window_shortcut()
849 self.init_window_shortcut()
710
850
711 def init_window_shortcut(self):
851 def init_window_shortcut(self):
712
852
713 self.fullScreenAct = QtGui.QAction("Full Screen",
853 self.fullScreenAct = QtGui.QAction("Full Screen",
714 self.window,
854 self.window,
715 shortcut="Ctrl+Meta+Space",
855 shortcut="Ctrl+Meta+Space",
716 statusTip="Toggle between Fullscreen and Normal Size",
856 statusTip="Toggle between Fullscreen and Normal Size",
717 triggered=self.toggleFullScreen)
857 triggered=self.toggleFullScreen)
718
858
859 self.tabAndNewKernelAct =QtGui.QAction("Tab with New kernel",
860 self.window,
861 shortcut="Ctrl+T",
862 triggered=self.createTabWithNewFrontend)
863 self.window.windowMenu.addAction(self.tabAndNewKernelAct)
864 self.tabSameKernalAct =QtGui.QAction("Tab with Same kernel",
865 self.window,
866 shortcut="Ctrl+Shift+T",
867 triggered=self.createTabAttachedToCurrentTabKernel)
868 self.window.windowMenu.addAction(self.tabSameKernalAct)
869 self.window.windowMenu.addSeparator()
719
870
720 # creating shortcut in menubar only for Mac OS as I don't
871 # creating shortcut in menubar only for Mac OS as I don't
721 # know the shortcut or if the windows manager assign it in
872 # know the shortcut or if the windows manager assign it in
722 # other platform.
873 # other platform.
723 if sys.platform == 'darwin':
874 if sys.platform == 'darwin':
724 self.minimizeAct = QtGui.QAction("Minimize",
875 self.minimizeAct = QtGui.QAction("Minimize",
725 self.window,
876 self.window,
726 shortcut="Ctrl+m",
877 shortcut="Ctrl+m",
727 statusTip="Minimize the window/Restore Normal Size",
878 statusTip="Minimize the window/Restore Normal Size",
728 triggered=self.toggleMinimized)
879 triggered=self.toggleMinimized)
729 self.maximizeAct = QtGui.QAction("Maximize",
880 self.maximizeAct = QtGui.QAction("Maximize",
730 self.window,
881 self.window,
731 shortcut="Ctrl+Shift+M",
882 shortcut="Ctrl+Shift+M",
732 statusTip="Maximize the window/Restore Normal Size",
883 statusTip="Maximize the window/Restore Normal Size",
733 triggered=self.toggleMaximized)
884 triggered=self.toggleMaximized)
734
885
735 self.onlineHelpAct = QtGui.QAction("Open Online Help",
886 self.onlineHelpAct = QtGui.QAction("Open Online Help",
736 self.window,
887 self.window,
737 triggered=self._open_online_help)
888 triggered=self._open_online_help)
738
889
739 self.windowMenu = self.window.windowMenu
890 self.windowMenu = self.window.windowMenu
740 self.windowMenu.addAction(self.minimizeAct)
891 self.windowMenu.addAction(self.minimizeAct)
741 self.windowMenu.addAction(self.maximizeAct)
892 self.windowMenu.addAction(self.maximizeAct)
742 self.windowMenu.addSeparator()
893 self.windowMenu.addSeparator()
743 self.windowMenu.addAction(self.fullScreenAct)
894 self.windowMenu.addAction(self.fullScreenAct)
744
895
745 self.window.helpMenu.addAction(self.onlineHelpAct)
896 self.window.helpMenu.addAction(self.onlineHelpAct)
746 else:
897 else:
747 # if we don't put it in a menu, we add it to the window so
898 # if we don't put it in a menu, we add it to the window so
748 # that it can still be triggerd by shortcut
899 # that it can still be triggerd by shortcut
749 self.window.addAction(self.fullScreenAct)
900 self.window.addAction(self.fullScreenAct)
750
901
751 def toggleMinimized(self):
902 def toggleMinimized(self):
752 if not self.window.isMinimized():
903 if not self.window.isMinimized():
753 self.window.showMinimized()
904 self.window.showMinimized()
754 else:
905 else:
755 self.window.showNormal()
906 self.window.showNormal()
756
907
757 def _open_online_help(self):
908 def _open_online_help(self):
758 QtGui.QDesktopServices.openUrl(
909 QtGui.QDesktopServices.openUrl(
759 QtCore.QUrl("http://ipython.org/documentation.html",
910 QtCore.QUrl("http://ipython.org/documentation.html",
760 QtCore.QUrl.TolerantMode)
911 QtCore.QUrl.TolerantMode)
761 )
912 )
762
913
763 def toggleMaximized(self):
914 def toggleMaximized(self):
764 if not self.window.isMaximized():
915 if not self.window.isMaximized():
765 self.window.showMaximized()
916 self.window.showMaximized()
766 else:
917 else:
767 self.window.showNormal()
918 self.window.showNormal()
768
919
769 # Min/Max imizing while in full screen give a bug
920 # Min/Max imizing while in full screen give a bug
770 # when going out of full screen, at least on OSX
921 # when going out of full screen, at least on OSX
771 def toggleFullScreen(self):
922 def toggleFullScreen(self):
772 if not self.window.isFullScreen():
923 if not self.window.isFullScreen():
773 self.window.showFullScreen()
924 self.window.showFullScreen()
774 if sys.platform == 'darwin':
925 if sys.platform == 'darwin':
775 self.maximizeAct.setEnabled(False)
926 self.maximizeAct.setEnabled(False)
776 self.minimizeAct.setEnabled(False)
927 self.minimizeAct.setEnabled(False)
777 else:
928 else:
778 self.window.showNormal()
929 self.window.showNormal()
779 if sys.platform == 'darwin':
930 if sys.platform == 'darwin':
780 self.maximizeAct.setEnabled(True)
931 self.maximizeAct.setEnabled(True)
781 self.minimizeAct.setEnabled(True)
932 self.minimizeAct.setEnabled(True)
782
933
783 def start(self):
934 def start(self):
784
935
785 # draw the window
936 # draw the window
786 self.window.show()
937 self.window.show()
787
938
788 # Start the application main loop.
939 # Start the application main loop.
789 self.app.exec_()
940 self.app.exec_()
790
941
791 #-----------------------------------------------------------------------------
942 #-----------------------------------------------------------------------------
792 # Main entry point
943 # Main entry point
793 #-----------------------------------------------------------------------------
944 #-----------------------------------------------------------------------------
794
945
795 def main():
946 def main():
796 app = IPythonQtConsoleApp()
947 app = IPythonQtConsoleApp()
797 app.initialize()
948 app.initialize()
798 app.start()
949 app.start()
799
950
800
951
801 if __name__ == '__main__':
952 if __name__ == '__main__':
802 main()
953 main()
General Comments 0
You need to be logged in to leave comments. Login now