##// END OF EJS Templates
fix @fperez suggestions
Matthias BUSSONNIER -
Show More
@@ -1,708 +1,711 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7 import uuid
8 8
9 9 # System library imports
10 10 from pygments.lexers import PythonLexer
11 11 from IPython.external import qt
12 12 from IPython.external.qt import QtCore, QtGui
13 13
14 14 # Local imports
15 15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
16 16 from IPython.core.oinspect import call_tip
17 17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 19 from bracket_matcher import BracketMatcher
20 20 from call_tip_widget import CallTipWidget
21 21 from completion_lexer import CompletionLexer
22 22 from history_console_widget import HistoryConsoleWidget
23 23 from pygments_highlighter import PygmentsHighlighter
24 24
25 25
26 26 class FrontendHighlighter(PygmentsHighlighter):
27 27 """ A PygmentsHighlighter that understands and ignores prompts.
28 28 """
29 29
30 30 def __init__(self, frontend):
31 31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 32 self._current_offset = 0
33 33 self._frontend = frontend
34 34 self.highlighting_on = False
35 35
36 36 def highlightBlock(self, string):
37 37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 38 """
39 39 if not self.highlighting_on:
40 40 return
41 41
42 42 # The input to this function is a unicode string that may contain
43 43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 44 # the string as plain text so we can compare it.
45 45 current_block = self.currentBlock()
46 46 string = self._frontend._get_block_plain_text(current_block)
47 47
48 48 # Decide whether to check for the regular or continuation prompt.
49 49 if current_block.contains(self._frontend._prompt_pos):
50 50 prompt = self._frontend._prompt
51 51 else:
52 52 prompt = self._frontend._continuation_prompt
53 53
54 54 # Only highlight if we can identify a prompt, but make sure not to
55 55 # highlight the prompt.
56 56 if string.startswith(prompt):
57 57 self._current_offset = len(prompt)
58 58 string = string[len(prompt):]
59 59 super(FrontendHighlighter, self).highlightBlock(string)
60 60
61 61 def rehighlightBlock(self, block):
62 62 """ Reimplemented to temporarily enable highlighting if disabled.
63 63 """
64 64 old = self.highlighting_on
65 65 self.highlighting_on = True
66 66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 67 self.highlighting_on = old
68 68
69 69 def setFormat(self, start, count, format):
70 70 """ Reimplemented to highlight selectively.
71 71 """
72 72 start += self._current_offset
73 73 super(FrontendHighlighter, self).setFormat(start, count, format)
74 74
75 75
76 76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 77 """ A Qt frontend for a generic Python kernel.
78 78 """
79 79
80 80 # The text to show when the kernel is (re)started.
81 81 banner = Unicode()
82 82
83 83 # An option and corresponding signal for overriding the default kernel
84 84 # interrupt behavior.
85 85 custom_interrupt = Bool(False)
86 86 custom_interrupt_requested = QtCore.Signal()
87 87
88 88 # An option and corresponding signals for overriding the default kernel
89 89 # restart behavior.
90 90 custom_restart = Bool(False)
91 91 custom_restart_kernel_died = QtCore.Signal(float)
92 92 custom_restart_requested = QtCore.Signal()
93 93
94 94 # Whether to automatically show calltips on open-parentheses.
95 95 enable_calltips = Bool(True, config=True,
96 96 help="Whether to draw information calltips on open-parentheses.")
97 97
98 98 # Emitted when a user visible 'execute_request' has been submitted to the
99 99 # kernel from the FrontendWidget. Contains the code to be executed.
100 100 executing = QtCore.Signal(object)
101 101
102 102 # Emitted when a user-visible 'execute_reply' has been received from the
103 103 # kernel and processed by the FrontendWidget. Contains the response message.
104 104 executed = QtCore.Signal(object)
105 105
106 106 # Emitted when an exit request has been received from the kernel.
107 107 exit_requested = QtCore.Signal(object)
108 108
109 109 # Protected class variables.
110 110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 113 _input_splitter_class = InputSplitter
114 114 _local_kernel = False
115 115 _highlighter = Instance(FrontendHighlighter)
116 116
117 117 #---------------------------------------------------------------------------
118 118 # 'object' interface
119 119 #---------------------------------------------------------------------------
120 120
121 121 def __init__(self, *args, **kw):
122 122 super(FrontendWidget, self).__init__(*args, **kw)
123 123 # FIXME: remove this when PySide min version is updated past 1.0.7
124 124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 125 if qt.QT_API == qt.QT_API_PYSIDE:
126 126 import PySide
127 127 if PySide.__version_info__ < (1,0,7):
128 128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 129 self.enable_calltips = False
130 130
131 131 # FrontendWidget protected variables.
132 132 self._bracket_matcher = BracketMatcher(self._control)
133 133 self._call_tip_widget = CallTipWidget(self._control)
134 134 self._completion_lexer = CompletionLexer(PythonLexer())
135 135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 136 self._hidden = False
137 137 self._highlighter = FrontendHighlighter(self)
138 138 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 139 self._kernel_manager = None
140 140 self._request_info = {}
141 self._callback_dict=dict([])
141 self._callback_dict = {}
142 142
143 143 # Configure the ConsoleWidget.
144 144 self.tab_width = 4
145 145 self._set_continuation_prompt('... ')
146 146
147 147 # Configure the CallTipWidget.
148 148 self._call_tip_widget.setFont(self.font)
149 149 self.font_changed.connect(self._call_tip_widget.setFont)
150 150
151 151 # Configure actions.
152 152 action = self._copy_raw_action
153 153 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
154 154 action.setEnabled(False)
155 155 action.setShortcut(QtGui.QKeySequence(key))
156 156 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
157 157 action.triggered.connect(self.copy_raw)
158 158 self.copy_available.connect(action.setEnabled)
159 159 self.addAction(action)
160 160
161 161 # Connect signal handlers.
162 162 document = self._control.document()
163 163 document.contentsChange.connect(self._document_contents_change)
164 164
165 165 # Set flag for whether we are connected via localhost.
166 166 self._local_kernel = kw.get('local_kernel',
167 167 FrontendWidget._local_kernel)
168 168
169 169 #---------------------------------------------------------------------------
170 170 # 'ConsoleWidget' public interface
171 171 #---------------------------------------------------------------------------
172 172
173 173 def copy(self):
174 174 """ Copy the currently selected text to the clipboard, removing prompts.
175 175 """
176 176 text = self._control.textCursor().selection().toPlainText()
177 177 if text:
178 178 lines = map(transform_classic_prompt, text.splitlines())
179 179 text = '\n'.join(lines)
180 180 QtGui.QApplication.clipboard().setText(text)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'ConsoleWidget' abstract interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def _is_complete(self, source, interactive):
187 187 """ Returns whether 'source' can be completely processed and a new
188 188 prompt created. When triggered by an Enter/Return key press,
189 189 'interactive' is True; otherwise, it is False.
190 190 """
191 191 complete = self._input_splitter.push(source)
192 192 if interactive:
193 193 complete = not self._input_splitter.push_accepts_more()
194 194 return complete
195 195
196 196 def _execute(self, source, hidden):
197 197 """ Execute 'source'. If 'hidden', do not show any output.
198 198
199 199 See parent class :meth:`execute` docstring for full details.
200 200 """
201 201 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
202 202 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
203 203 self._hidden = hidden
204 204 if not hidden:
205 205 self.executing.emit(source)
206 206
207 207 def _prompt_started_hook(self):
208 208 """ Called immediately after a new prompt is displayed.
209 209 """
210 210 if not self._reading:
211 211 self._highlighter.highlighting_on = True
212 212
213 213 def _prompt_finished_hook(self):
214 214 """ Called immediately after a prompt is finished, i.e. when some input
215 215 will be processed and a new prompt displayed.
216 216 """
217 217 # Flush all state from the input splitter so the next round of
218 218 # reading input starts with a clean buffer.
219 219 self._input_splitter.reset()
220 220
221 221 if not self._reading:
222 222 self._highlighter.highlighting_on = False
223 223
224 224 def _tab_pressed(self):
225 225 """ Called when the tab key is pressed. Returns whether to continue
226 226 processing the event.
227 227 """
228 228 # Perform tab completion if:
229 229 # 1) The cursor is in the input buffer.
230 230 # 2) There is a non-whitespace character before the cursor.
231 231 text = self._get_input_buffer_cursor_line()
232 232 if text is None:
233 233 return False
234 234 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
235 235 if complete:
236 236 self._complete()
237 237 return not complete
238 238
239 239 #---------------------------------------------------------------------------
240 240 # 'ConsoleWidget' protected interface
241 241 #---------------------------------------------------------------------------
242 242
243 243 def _context_menu_make(self, pos):
244 244 """ Reimplemented to add an action for raw copy.
245 245 """
246 246 menu = super(FrontendWidget, self)._context_menu_make(pos)
247 247 for before_action in menu.actions():
248 248 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
249 249 QtGui.QKeySequence.ExactMatch:
250 250 menu.insertAction(before_action, self._copy_raw_action)
251 251 break
252 252 return menu
253 253
254 254 def request_interrupt_kernel(self):
255 255 if self._executing:
256 256 self.interrupt_kernel()
257 257
258 258 def request_restart_kernel(self):
259 259 message = 'Are you sure you want to restart the kernel?'
260 260 self.restart_kernel(message, now=False)
261 261
262 262 def _event_filter_console_keypress(self, event):
263 263 """ Reimplemented for execution interruption and smart backspace.
264 264 """
265 265 key = event.key()
266 266 if self._control_key_down(event.modifiers(), include_command=False):
267 267
268 268 if key == QtCore.Qt.Key_C and self._executing:
269 269 self.request_interrupt_kernel()
270 270 return True
271 271
272 272 elif key == QtCore.Qt.Key_Period:
273 273 self.request_restart_kernel()
274 274 return True
275 275
276 276 elif not event.modifiers() & QtCore.Qt.AltModifier:
277 277
278 278 # Smart backspace: remove four characters in one backspace if:
279 279 # 1) everything left of the cursor is whitespace
280 280 # 2) the four characters immediately left of the cursor are spaces
281 281 if key == QtCore.Qt.Key_Backspace:
282 282 col = self._get_input_buffer_cursor_column()
283 283 cursor = self._control.textCursor()
284 284 if col > 3 and not cursor.hasSelection():
285 285 text = self._get_input_buffer_cursor_line()[:col]
286 286 if text.endswith(' ') and not text.strip():
287 287 cursor.movePosition(QtGui.QTextCursor.Left,
288 288 QtGui.QTextCursor.KeepAnchor, 4)
289 289 cursor.removeSelectedText()
290 290 return True
291 291
292 292 return super(FrontendWidget, self)._event_filter_console_keypress(event)
293 293
294 294 def _insert_continuation_prompt(self, cursor):
295 295 """ Reimplemented for auto-indentation.
296 296 """
297 297 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
298 298 cursor.insertText(' ' * self._input_splitter.indent_spaces)
299 299
300 300 #---------------------------------------------------------------------------
301 301 # 'BaseFrontendMixin' abstract interface
302 302 #---------------------------------------------------------------------------
303 303
304 304 def _handle_complete_reply(self, rep):
305 305 """ Handle replies for tab completion.
306 306 """
307 307 self.log.debug("complete: %s", rep.get('content', ''))
308 308 cursor = self._get_cursor()
309 309 info = self._request_info.get('complete')
310 310 if info and info.id == rep['parent_header']['msg_id'] and \
311 311 info.pos == cursor.position():
312 312 text = '.'.join(self._get_context())
313 313 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
314 314 self._complete_with_items(cursor, rep['content']['matches'])
315 315
316 def _silent_exec_callback(self,expr,callback):
317 """ silently execute a function in the kernel and send the reply to the callback
316 def _silent_exec_callback(self, expr, callback):
317 """Silently execute `expr` in the kernel and call `callback` with reply
318 318
319 expr should be a valid string to be executed by the kernel.
319 `expr` : valid string to be executed by the kernel.
320 `callback` : function accepting one string as argument.
320 321
321 callback a function accepting one argument (str)
322
323 the callback is called with the 'repr()' of the result as first argument.
324 to get the object, do 'eval()' on the passed value.
322 The `callback` is called with the 'repr()' of the result of `expr` as
323 first argument. To get the object, do 'eval()' on the passed value.
325 324 """
326 325
327 326 # generate uuid, which would be used as a indication of wether or not
328 327 # the unique request originate from here (can use msg id ?)
329 328 local_uuid = str(uuid.uuid1())
330 329 msg_id = self.kernel_manager.shell_channel.execute('',
331 silent=True,
332 user_expressions={ local_uuid:expr,
333 }
334 )
335 self._callback_dict[local_uuid]=callback
330 silent=True, user_expressions={ local_uuid:expr })
331 self._callback_dict[local_uuid] = callback
336 332 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
337 333
338 def _handle_exec_callback(self,msg):
339 """ Called when _silent_exec_callback message comme back from the kernel.
334 def _handle_exec_callback(self, msg):
335 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
336
337 `msg` : raw message send by the kernel containing an `user_expressions`
338 and having a 'silent_exec_callback' kind.
340 339
341 Strip the message comming back from the kernel and send it to the
342 corresponding callback function.
340 This fonction will look for a `callback` associated with the
341 corresponding message id. Association has been made by
342 ``_silent_exec_callback``. `callback`is then called with the `repr()`
343 of the value of corresponding `user_expressions` as argument.
344 `callback` is then removed from the known list so that any message
345 coming again with the same id won't trigger it.
343 346 """
344 cnt=msg['content']
345 ue=cnt['user_expressions']
347
348 cnt = msg['content']
349 ue = cnt['user_expressions']
346 350 for i in ue.keys():
347 if i in self._callback_dict.keys():
348 f= self._callback_dict[i]
349 f(ue[i])
351 if i in self._callback_dict:
352 self._callback_dict[i](ue[i])
350 353 self._callback_dict.pop(i)
351 354
352 355 def _handle_execute_reply(self, msg):
353 356 """ Handles replies for code execution.
354 357 """
355 358 self.log.debug("execute: %s", msg.get('content', ''))
356 359 info = self._request_info.get('execute')
357 360 # unset reading flag, because if execute finished, raw_input can't
358 361 # still be pending.
359 362 self._reading = False
360 363 if info and info.id == msg['parent_header']['msg_id'] and \
361 364 info.kind == 'user' and not self._hidden:
362 365 # Make sure that all output from the SUB channel has been processed
363 366 # before writing a new prompt.
364 367 self.kernel_manager.sub_channel.flush()
365 368
366 369 # Reset the ANSI style information to prevent bad text in stdout
367 370 # from messing up our colors. We're not a true terminal so we're
368 371 # allowed to do this.
369 372 if self.ansi_codes:
370 373 self._ansi_processor.reset_sgr()
371 374
372 375 content = msg['content']
373 376 status = content['status']
374 377 if status == 'ok':
375 378 self._process_execute_ok(msg)
376 379 elif status == 'error':
377 380 self._process_execute_error(msg)
378 381 elif status == 'aborted':
379 382 self._process_execute_abort(msg)
380 383
381 384 self._show_interpreter_prompt_for_reply(msg)
382 385 self.executed.emit(msg)
383 386 elif info and info.id == msg['parent_header']['msg_id'] and \
384 387 info.kind == 'silent_exec_callback' and not self._hidden:
385 388 self._handle_exec_callback(msg)
386 389 else:
387 390 super(FrontendWidget, self)._handle_execute_reply(msg)
388 391
389 392 def _handle_input_request(self, msg):
390 393 """ Handle requests for raw_input.
391 394 """
392 395 self.log.debug("input: %s", msg.get('content', ''))
393 396 if self._hidden:
394 397 raise RuntimeError('Request for raw input during hidden execution.')
395 398
396 399 # Make sure that all output from the SUB channel has been processed
397 400 # before entering readline mode.
398 401 self.kernel_manager.sub_channel.flush()
399 402
400 403 def callback(line):
401 404 self.kernel_manager.stdin_channel.input(line)
402 405 if self._reading:
403 406 self.log.debug("Got second input request, assuming first was interrupted.")
404 407 self._reading = False
405 408 self._readline(msg['content']['prompt'], callback=callback)
406 409
407 410 def _handle_kernel_died(self, since_last_heartbeat):
408 411 """ Handle the kernel's death by asking if the user wants to restart.
409 412 """
410 413 self.log.debug("kernel died: %s", since_last_heartbeat)
411 414 if self.custom_restart:
412 415 self.custom_restart_kernel_died.emit(since_last_heartbeat)
413 416 else:
414 417 message = 'The kernel heartbeat has been inactive for %.2f ' \
415 418 'seconds. Do you want to restart the kernel? You may ' \
416 419 'first want to check the network connection.' % \
417 420 since_last_heartbeat
418 421 self.restart_kernel(message, now=True)
419 422
420 423 def _handle_object_info_reply(self, rep):
421 424 """ Handle replies for call tips.
422 425 """
423 426 self.log.debug("oinfo: %s", rep.get('content', ''))
424 427 cursor = self._get_cursor()
425 428 info = self._request_info.get('call_tip')
426 429 if info and info.id == rep['parent_header']['msg_id'] and \
427 430 info.pos == cursor.position():
428 431 # Get the information for a call tip. For now we format the call
429 432 # line as string, later we can pass False to format_call and
430 433 # syntax-highlight it ourselves for nicer formatting in the
431 434 # calltip.
432 435 content = rep['content']
433 436 # if this is from pykernel, 'docstring' will be the only key
434 437 if content.get('ismagic', False):
435 438 # Don't generate a call-tip for magics. Ideally, we should
436 439 # generate a tooltip, but not on ( like we do for actual
437 440 # callables.
438 441 call_info, doc = None, None
439 442 else:
440 443 call_info, doc = call_tip(content, format_call=True)
441 444 if call_info or doc:
442 445 self._call_tip_widget.show_call_info(call_info, doc)
443 446
444 447 def _handle_pyout(self, msg):
445 448 """ Handle display hook output.
446 449 """
447 450 self.log.debug("pyout: %s", msg.get('content', ''))
448 451 if not self._hidden and self._is_from_this_session(msg):
449 452 text = msg['content']['data']
450 453 self._append_plain_text(text + '\n', before_prompt=True)
451 454
452 455 def _handle_stream(self, msg):
453 456 """ Handle stdout, stderr, and stdin.
454 457 """
455 458 self.log.debug("stream: %s", msg.get('content', ''))
456 459 if not self._hidden and self._is_from_this_session(msg):
457 460 # Most consoles treat tabs as being 8 space characters. Convert tabs
458 461 # to spaces so that output looks as expected regardless of this
459 462 # widget's tab width.
460 463 text = msg['content']['data'].expandtabs(8)
461 464
462 465 self._append_plain_text(text, before_prompt=True)
463 466 self._control.moveCursor(QtGui.QTextCursor.End)
464 467
465 468 def _handle_shutdown_reply(self, msg):
466 469 """ Handle shutdown signal, only if from other console.
467 470 """
468 471 self.log.debug("shutdown: %s", msg.get('content', ''))
469 472 if not self._hidden and not self._is_from_this_session(msg):
470 473 if self._local_kernel:
471 474 if not msg['content']['restart']:
472 475 self.exit_requested.emit(self)
473 476 else:
474 477 # we just got notified of a restart!
475 478 time.sleep(0.25) # wait 1/4 sec to reset
476 479 # lest the request for a new prompt
477 480 # goes to the old kernel
478 481 self.reset()
479 482 else: # remote kernel, prompt on Kernel shutdown/reset
480 483 title = self.window().windowTitle()
481 484 if not msg['content']['restart']:
482 485 reply = QtGui.QMessageBox.question(self, title,
483 486 "Kernel has been shutdown permanently. "
484 487 "Close the Console?",
485 488 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
486 489 if reply == QtGui.QMessageBox.Yes:
487 490 self.exit_requested.emit(self)
488 491 else:
489 492 reply = QtGui.QMessageBox.question(self, title,
490 493 "Kernel has been reset. Clear the Console?",
491 494 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
492 495 if reply == QtGui.QMessageBox.Yes:
493 496 time.sleep(0.25) # wait 1/4 sec to reset
494 497 # lest the request for a new prompt
495 498 # goes to the old kernel
496 499 self.reset()
497 500
498 501 def _started_channels(self):
499 502 """ Called when the KernelManager channels have started listening or
500 503 when the frontend is assigned an already listening KernelManager.
501 504 """
502 505 self.reset()
503 506
504 507 #---------------------------------------------------------------------------
505 508 # 'FrontendWidget' public interface
506 509 #---------------------------------------------------------------------------
507 510
508 511 def copy_raw(self):
509 512 """ Copy the currently selected text to the clipboard without attempting
510 513 to remove prompts or otherwise alter the text.
511 514 """
512 515 self._control.copy()
513 516
514 517 def execute_file(self, path, hidden=False):
515 518 """ Attempts to execute file with 'path'. If 'hidden', no output is
516 519 shown.
517 520 """
518 521 self.execute('execfile(%r)' % path, hidden=hidden)
519 522
520 523 def interrupt_kernel(self):
521 524 """ Attempts to interrupt the running kernel.
522 525
523 526 Also unsets _reading flag, to avoid runtime errors
524 527 if raw_input is called again.
525 528 """
526 529 if self.custom_interrupt:
527 530 self._reading = False
528 531 self.custom_interrupt_requested.emit()
529 532 elif self.kernel_manager.has_kernel:
530 533 self._reading = False
531 534 self.kernel_manager.interrupt_kernel()
532 535 else:
533 536 self._append_plain_text('Kernel process is either remote or '
534 537 'unspecified. Cannot interrupt.\n')
535 538
536 539 def reset(self):
537 540 """ Resets the widget to its initial state. Similar to ``clear``, but
538 541 also re-writes the banner and aborts execution if necessary.
539 542 """
540 543 if self._executing:
541 544 self._executing = False
542 545 self._request_info['execute'] = None
543 546 self._reading = False
544 547 self._highlighter.highlighting_on = False
545 548
546 549 self._control.clear()
547 550 self._append_plain_text(self.banner)
548 551 # update output marker for stdout/stderr, so that startup
549 552 # messages appear after banner:
550 553 self._append_before_prompt_pos = self._get_cursor().position()
551 554 self._show_interpreter_prompt()
552 555
553 556 def restart_kernel(self, message, now=False):
554 557 """ Attempts to restart the running kernel.
555 558 """
556 559 # FIXME: now should be configurable via a checkbox in the dialog. Right
557 560 # now at least the heartbeat path sets it to True and the manual restart
558 561 # to False. But those should just be the pre-selected states of a
559 562 # checkbox that the user could override if so desired. But I don't know
560 563 # enough Qt to go implementing the checkbox now.
561 564
562 565 if self.custom_restart:
563 566 self.custom_restart_requested.emit()
564 567
565 568 elif self.kernel_manager.has_kernel:
566 569 # Pause the heart beat channel to prevent further warnings.
567 570 self.kernel_manager.hb_channel.pause()
568 571
569 572 # Prompt the user to restart the kernel. Un-pause the heartbeat if
570 573 # they decline. (If they accept, the heartbeat will be un-paused
571 574 # automatically when the kernel is restarted.)
572 575 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
573 576 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
574 577 message, buttons)
575 578 if result == QtGui.QMessageBox.Yes:
576 579 try:
577 580 self.kernel_manager.restart_kernel(now=now)
578 581 except RuntimeError:
579 582 self._append_plain_text('Kernel started externally. '
580 583 'Cannot restart.\n')
581 584 else:
582 585 self.reset()
583 586 else:
584 587 self.kernel_manager.hb_channel.unpause()
585 588
586 589 else:
587 590 self._append_plain_text('Kernel process is either remote or '
588 591 'unspecified. Cannot restart.\n')
589 592
590 593 #---------------------------------------------------------------------------
591 594 # 'FrontendWidget' protected interface
592 595 #---------------------------------------------------------------------------
593 596
594 597 def _call_tip(self):
595 598 """ Shows a call tip, if appropriate, at the current cursor location.
596 599 """
597 600 # Decide if it makes sense to show a call tip
598 601 if not self.enable_calltips:
599 602 return False
600 603 cursor = self._get_cursor()
601 604 cursor.movePosition(QtGui.QTextCursor.Left)
602 605 if cursor.document().characterAt(cursor.position()) != '(':
603 606 return False
604 607 context = self._get_context(cursor)
605 608 if not context:
606 609 return False
607 610
608 611 # Send the metadata request to the kernel
609 612 name = '.'.join(context)
610 613 msg_id = self.kernel_manager.shell_channel.object_info(name)
611 614 pos = self._get_cursor().position()
612 615 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
613 616 return True
614 617
615 618 def _complete(self):
616 619 """ Performs completion at the current cursor location.
617 620 """
618 621 context = self._get_context()
619 622 if context:
620 623 # Send the completion request to the kernel
621 624 msg_id = self.kernel_manager.shell_channel.complete(
622 625 '.'.join(context), # text
623 626 self._get_input_buffer_cursor_line(), # line
624 627 self._get_input_buffer_cursor_column(), # cursor_pos
625 628 self.input_buffer) # block
626 629 pos = self._get_cursor().position()
627 630 info = self._CompletionRequest(msg_id, pos)
628 631 self._request_info['complete'] = info
629 632
630 633 def _get_context(self, cursor=None):
631 634 """ Gets the context for the specified cursor (or the current cursor
632 635 if none is specified).
633 636 """
634 637 if cursor is None:
635 638 cursor = self._get_cursor()
636 639 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
637 640 QtGui.QTextCursor.KeepAnchor)
638 641 text = cursor.selection().toPlainText()
639 642 return self._completion_lexer.get_context(text)
640 643
641 644 def _process_execute_abort(self, msg):
642 645 """ Process a reply for an aborted execution request.
643 646 """
644 647 self._append_plain_text("ERROR: execution aborted\n")
645 648
646 649 def _process_execute_error(self, msg):
647 650 """ Process a reply for an execution request that resulted in an error.
648 651 """
649 652 content = msg['content']
650 653 # If a SystemExit is passed along, this means exit() was called - also
651 654 # all the ipython %exit magic syntax of '-k' to be used to keep
652 655 # the kernel running
653 656 if content['ename']=='SystemExit':
654 657 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
655 658 self._keep_kernel_on_exit = keepkernel
656 659 self.exit_requested.emit(self)
657 660 else:
658 661 traceback = ''.join(content['traceback'])
659 662 self._append_plain_text(traceback)
660 663
661 664 def _process_execute_ok(self, msg):
662 665 """ Process a reply for a successful execution equest.
663 666 """
664 667 payload = msg['content']['payload']
665 668 for item in payload:
666 669 if not self._process_execute_payload(item):
667 670 warning = 'Warning: received unknown payload of type %s'
668 671 print(warning % repr(item['source']))
669 672
670 673 def _process_execute_payload(self, item):
671 674 """ Process a single payload item from the list of payload items in an
672 675 execution reply. Returns whether the payload was handled.
673 676 """
674 677 # The basic FrontendWidget doesn't handle payloads, as they are a
675 678 # mechanism for going beyond the standard Python interpreter model.
676 679 return False
677 680
678 681 def _show_interpreter_prompt(self):
679 682 """ Shows a prompt for the interpreter.
680 683 """
681 684 self._show_prompt('>>> ')
682 685
683 686 def _show_interpreter_prompt_for_reply(self, msg):
684 687 """ Shows a prompt for the interpreter given an 'execute_reply' message.
685 688 """
686 689 self._show_interpreter_prompt()
687 690
688 691 #------ Signal handlers ----------------------------------------------------
689 692
690 693 def _document_contents_change(self, position, removed, added):
691 694 """ Called whenever the document's content changes. Display a call tip
692 695 if appropriate.
693 696 """
694 697 # Calculate where the cursor should be *after* the change:
695 698 position += added
696 699
697 700 document = self._control.document()
698 701 if position == self._get_cursor().position():
699 702 self._call_tip()
700 703
701 704 #------ Trait default initializers -----------------------------------------
702 705
703 706 def _banner_default(self):
704 707 """ Returns the standard Python banner.
705 708 """
706 709 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
707 710 '"license" for more information.'
708 711 return banner % (sys.version, sys.platform)
@@ -1,858 +1,879 b''
1 1 """The Qt MainWindow for the QtConsole
2 2
3 3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 4 common actions.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import sys
23 23 import webbrowser
24 24 from threading import Thread
25 25
26 26 # System library imports
27 27 from IPython.external.qt import QtGui,QtCore
28 28
29 29 def background(f):
30 30 """call a function in a simple thread, to prevent blocking"""
31 31 t = Thread(target=f)
32 32 t.start()
33 33 return t
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Classes
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class MainWindow(QtGui.QMainWindow):
40 40
41 41 #---------------------------------------------------------------------------
42 42 # 'object' interface
43 43 #---------------------------------------------------------------------------
44 44
45 45 def __init__(self, app,
46 46 confirm_exit=True,
47 47 new_frontend_factory=None, slave_frontend_factory=None,
48 48 ):
49 49 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
50 50
51 51 Parameters
52 52 ----------
53 53
54 54 app : reference to QApplication parent
55 55 confirm_exit : bool, optional
56 56 Whether we should prompt on close of tabs
57 57 new_frontend_factory : callable
58 58 A callable that returns a new IPythonWidget instance, attached to
59 59 its own running kernel.
60 60 slave_frontend_factory : callable
61 61 A callable that takes an existing IPythonWidget, and returns a new
62 62 IPythonWidget instance, attached to the same kernel.
63 63 """
64 64
65 65 super(MainWindow, self).__init__()
66 66 self._kernel_counter = 0
67 67 self._app = app
68 68 self.confirm_exit = confirm_exit
69 69 self.new_frontend_factory = new_frontend_factory
70 70 self.slave_frontend_factory = slave_frontend_factory
71 71
72 72 self.tab_widget = QtGui.QTabWidget(self)
73 73 self.tab_widget.setDocumentMode(True)
74 74 self.tab_widget.setTabsClosable(True)
75 75 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
76 76
77 77 self.setCentralWidget(self.tab_widget)
78 78 # hide tab bar at first, since we have no tabs:
79 79 self.tab_widget.tabBar().setVisible(False)
80 80 # prevent focus in tab bar
81 81 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
82 82
83 83 def update_tab_bar_visibility(self):
84 84 """ update visibility of the tabBar depending of the number of tab
85 85
86 86 0 or 1 tab, tabBar hidden
87 87 2+ tabs, tabBar visible
88 88
89 89 send a self.close if number of tab ==0
90 90
91 91 need to be called explicitely, or be connected to tabInserted/tabRemoved
92 92 """
93 93 if self.tab_widget.count() <= 1:
94 94 self.tab_widget.tabBar().setVisible(False)
95 95 else:
96 96 self.tab_widget.tabBar().setVisible(True)
97 97 if self.tab_widget.count()==0 :
98 98 self.close()
99 99
100 100 @property
101 101 def next_kernel_id(self):
102 102 """constantly increasing counter for kernel IDs"""
103 103 c = self._kernel_counter
104 104 self._kernel_counter += 1
105 105 return c
106 106
107 107 @property
108 108 def active_frontend(self):
109 109 return self.tab_widget.currentWidget()
110 110
111 111 def create_tab_with_new_frontend(self):
112 112 """create a new frontend and attach it to a new tab"""
113 113 widget = self.new_frontend_factory()
114 114 self.add_tab_with_frontend(widget)
115 115
116 116 def create_tab_with_current_kernel(self):
117 117 """create a new frontend attached to the same kernel as the current tab"""
118 118 current_widget = self.tab_widget.currentWidget()
119 119 current_widget_index = self.tab_widget.indexOf(current_widget)
120 120 current_widget_name = self.tab_widget.tabText(current_widget_index)
121 121 widget = self.slave_frontend_factory(current_widget)
122 122 if 'slave' in current_widget_name:
123 123 # don't keep stacking slaves
124 124 name = current_widget_name
125 125 else:
126 126 name = '(%s) slave' % current_widget_name
127 127 self.add_tab_with_frontend(widget,name=name)
128 128
129 129 def close_tab(self,current_tab):
130 130 """ Called when you need to try to close a tab.
131 131
132 132 It takes the number of the tab to be closed as argument, or a referece
133 133 to the wiget insite this tab
134 134 """
135 135
136 136 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
137 137 # and a reference to the trontend to close
138 138 if type(current_tab) is not int :
139 139 current_tab = self.tab_widget.indexOf(current_tab)
140 140 closing_widget=self.tab_widget.widget(current_tab)
141 141
142 142
143 143 # when trying to be closed, widget might re-send a request to be closed again, but will
144 144 # be deleted when event will be processed. So need to check that widget still exist and
145 145 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
146 146 # re-send by this fonction on the master widget, which ask all slaves widget to exit
147 147 if closing_widget==None:
148 148 return
149 149
150 150 #get a list of all slave widgets on the same kernel.
151 151 slave_tabs = self.find_slave_widgets(closing_widget)
152 152
153 153 keepkernel = None #Use the prompt by default
154 154 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
155 155 keepkernel = closing_widget._keep_kernel_on_exit
156 156 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
157 157 # we set local slave tabs._hidden to True to avoid prompting for kernel
158 158 # restart when they get the signal. and then "forward" the 'exit'
159 159 # to the main window
160 160 if keepkernel is not None:
161 161 for tab in slave_tabs:
162 162 tab._hidden = True
163 163 if closing_widget in slave_tabs:
164 164 try :
165 165 self.find_master_tab(closing_widget).execute('exit')
166 166 except AttributeError:
167 167 self.log.info("Master already closed or not local, closing only current tab")
168 168 self.tab_widget.removeTab(current_tab)
169 169 self.update_tab_bar_visibility()
170 170 return
171 171
172 172 kernel_manager = closing_widget.kernel_manager
173 173
174 174 if keepkernel is None and not closing_widget._confirm_exit:
175 175 # don't prompt, just terminate the kernel if we own it
176 176 # or leave it alone if we don't
177 177 keepkernel = closing_widget._existing
178 178 if keepkernel is None: #show prompt
179 179 if kernel_manager and kernel_manager.channels_running:
180 180 title = self.window().windowTitle()
181 181 cancel = QtGui.QMessageBox.Cancel
182 182 okay = QtGui.QMessageBox.Ok
183 183 if closing_widget._may_close:
184 184 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
185 185 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
186 186 justthis = QtGui.QPushButton("&No, just this Tab", self)
187 187 justthis.setShortcut('N')
188 188 closeall = QtGui.QPushButton("&Yes, close all", self)
189 189 closeall.setShortcut('Y')
190 190 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
191 191 title, msg)
192 192 box.setInformativeText(info)
193 193 box.addButton(cancel)
194 194 box.addButton(justthis, QtGui.QMessageBox.NoRole)
195 195 box.addButton(closeall, QtGui.QMessageBox.YesRole)
196 196 box.setDefaultButton(closeall)
197 197 box.setEscapeButton(cancel)
198 198 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
199 199 box.setIconPixmap(pixmap)
200 200 reply = box.exec_()
201 201 if reply == 1: # close All
202 202 for slave in slave_tabs:
203 203 background(slave.kernel_manager.stop_channels)
204 204 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
205 205 closing_widget.execute("exit")
206 206 self.tab_widget.removeTab(current_tab)
207 207 background(kernel_manager.stop_channels)
208 208 elif reply == 0: # close Console
209 209 if not closing_widget._existing:
210 210 # Have kernel: don't quit, just close the tab
211 211 closing_widget.execute("exit True")
212 212 self.tab_widget.removeTab(current_tab)
213 213 background(kernel_manager.stop_channels)
214 214 else:
215 215 reply = QtGui.QMessageBox.question(self, title,
216 216 "Are you sure you want to close this Console?"+
217 217 "\nThe Kernel and other Consoles will remain active.",
218 218 okay|cancel,
219 219 defaultButton=okay
220 220 )
221 221 if reply == okay:
222 222 self.tab_widget.removeTab(current_tab)
223 223 elif keepkernel: #close console but leave kernel running (no prompt)
224 224 self.tab_widget.removeTab(current_tab)
225 225 background(kernel_manager.stop_channels)
226 226 else: #close console and kernel (no prompt)
227 227 self.tab_widget.removeTab(current_tab)
228 228 if kernel_manager and kernel_manager.channels_running:
229 229 for slave in slave_tabs:
230 230 background(slave.kernel_manager.stop_channels)
231 231 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
232 232 kernel_manager.shutdown_kernel()
233 233 background(kernel_manager.stop_channels)
234 234
235 235 self.update_tab_bar_visibility()
236 236
237 237 def add_tab_with_frontend(self,frontend,name=None):
238 238 """ insert a tab with a given frontend in the tab bar, and give it a name
239 239
240 240 """
241 241 if not name:
242 242 name = 'kernel %i' % self.next_kernel_id
243 243 self.tab_widget.addTab(frontend,name)
244 244 self.update_tab_bar_visibility()
245 245 self.make_frontend_visible(frontend)
246 246 frontend.exit_requested.connect(self.close_tab)
247 247
248 248 def next_tab(self):
249 249 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
250 250
251 251 def prev_tab(self):
252 252 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
253 253
254 254 def make_frontend_visible(self,frontend):
255 255 widget_index=self.tab_widget.indexOf(frontend)
256 256 if widget_index > 0 :
257 257 self.tab_widget.setCurrentIndex(widget_index)
258 258
259 259 def find_master_tab(self,tab,as_list=False):
260 260 """
261 261 Try to return the frontend that own the kernel attached to the given widget/tab.
262 262
263 263 Only find frontend owed by the current application. Selection
264 264 based on port of the kernel, might be inacurate if several kernel
265 265 on different ip use same port number.
266 266
267 267 This fonction does the conversion tabNumber/widget if needed.
268 268 Might return None if no master widget (non local kernel)
269 269 Will crash IPython if more than 1 masterWidget
270 270
271 271 When asList set to True, always return a list of widget(s) owning
272 272 the kernel. The list might be empty or containing several Widget.
273 273 """
274 274
275 275 #convert from/to int/richIpythonWidget if needed
276 276 if isinstance(tab, int):
277 277 tab = self.tab_widget.widget(tab)
278 278 km=tab.kernel_manager
279 279
280 280 #build list of all widgets
281 281 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
282 282
283 283 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
284 284 # And should have a _may_close attribute
285 285 filtered_widget_list = [ widget for widget in widget_list if
286 286 widget.kernel_manager.connection_file == km.connection_file and
287 287 hasattr(widget,'_may_close') ]
288 288 # the master widget is the one that may close the kernel
289 289 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
290 290 if as_list:
291 291 return master_widget
292 292 assert(len(master_widget)<=1 )
293 293 if len(master_widget)==0:
294 294 return None
295 295
296 296 return master_widget[0]
297 297
298 298 def find_slave_widgets(self,tab):
299 299 """return all the frontends that do not own the kernel attached to the given widget/tab.
300 300
301 301 Only find frontends owned by the current application. Selection
302 302 based on connection file of the kernel.
303 303
304 304 This function does the conversion tabNumber/widget if needed.
305 305 """
306 306 #convert from/to int/richIpythonWidget if needed
307 307 if isinstance(tab, int):
308 308 tab = self.tab_widget.widget(tab)
309 309 km=tab.kernel_manager
310 310
311 311 #build list of all widgets
312 312 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
313 313
314 314 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
315 315 filtered_widget_list = ( widget for widget in widget_list if
316 316 widget.kernel_manager.connection_file == km.connection_file)
317 317 # Get a list of all widget owning the same kernel and removed it from
318 318 # the previous cadidate. (better using sets ?)
319 319 master_widget_list = self.find_master_tab(tab, as_list=True)
320 320 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
321 321
322 322 return slave_list
323 323
324 324 # Populate the menu bar with common actions and shortcuts
325 325 def add_menu_action(self, menu, action, defer_shortcut=False):
326 326 """Add action to menu as well as self
327 327
328 328 So that when the menu bar is invisible, its actions are still available.
329 329
330 330 If defer_shortcut is True, set the shortcut context to widget-only,
331 331 where it will avoid conflict with shortcuts already bound to the
332 332 widgets themselves.
333 333 """
334 334 menu.addAction(action)
335 335 self.addAction(action)
336 336
337 337 if defer_shortcut:
338 338 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
339 339
340 340 def init_menu_bar(self):
341 341 #create menu in the order they should appear in the menu bar
342 342 self.init_file_menu()
343 343 self.init_edit_menu()
344 344 self.init_view_menu()
345 345 self.init_kernel_menu()
346 346 self.init_magic_menu()
347 347 self.init_window_menu()
348 348 self.init_help_menu()
349 349
350 350 def init_file_menu(self):
351 351 self.file_menu = self.menuBar().addMenu("&File")
352 352
353 353 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
354 354 self,
355 355 shortcut="Ctrl+T",
356 356 triggered=self.create_tab_with_new_frontend)
357 357 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
358 358
359 359 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
360 360 self,
361 361 shortcut="Ctrl+Shift+T",
362 362 triggered=self.create_tab_with_current_kernel)
363 363 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
364 364
365 365 self.file_menu.addSeparator()
366 366
367 367 self.close_action=QtGui.QAction("&Close Tab",
368 368 self,
369 369 shortcut=QtGui.QKeySequence.Close,
370 370 triggered=self.close_active_frontend
371 371 )
372 372 self.add_menu_action(self.file_menu, self.close_action)
373 373
374 374 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
375 375 self,
376 376 shortcut=QtGui.QKeySequence.Save,
377 377 triggered=self.export_action_active_frontend
378 378 )
379 379 self.add_menu_action(self.file_menu, self.export_action, True)
380 380
381 381 self.file_menu.addSeparator()
382 382
383 383 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
384 384 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
385 385 # Only override the default if there is a collision.
386 386 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
387 387 printkey = "Ctrl+Shift+P"
388 388 self.print_action = QtGui.QAction("&Print",
389 389 self,
390 390 shortcut=printkey,
391 391 triggered=self.print_action_active_frontend)
392 392 self.add_menu_action(self.file_menu, self.print_action, True)
393 393
394 394 if sys.platform != 'darwin':
395 395 # OSX always has Quit in the Application menu, only add it
396 396 # to the File menu elsewhere.
397 397
398 398 self.file_menu.addSeparator()
399 399
400 400 self.quit_action = QtGui.QAction("&Quit",
401 401 self,
402 402 shortcut=QtGui.QKeySequence.Quit,
403 403 triggered=self.close,
404 404 )
405 405 self.add_menu_action(self.file_menu, self.quit_action)
406 406
407 407
408 408 def init_edit_menu(self):
409 409 self.edit_menu = self.menuBar().addMenu("&Edit")
410 410
411 411 self.undo_action = QtGui.QAction("&Undo",
412 412 self,
413 413 shortcut=QtGui.QKeySequence.Undo,
414 414 statusTip="Undo last action if possible",
415 415 triggered=self.undo_active_frontend
416 416 )
417 417 self.add_menu_action(self.edit_menu, self.undo_action)
418 418
419 419 self.redo_action = QtGui.QAction("&Redo",
420 420 self,
421 421 shortcut=QtGui.QKeySequence.Redo,
422 422 statusTip="Redo last action if possible",
423 423 triggered=self.redo_active_frontend)
424 424 self.add_menu_action(self.edit_menu, self.redo_action)
425 425
426 426 self.edit_menu.addSeparator()
427 427
428 428 self.cut_action = QtGui.QAction("&Cut",
429 429 self,
430 430 shortcut=QtGui.QKeySequence.Cut,
431 431 triggered=self.cut_active_frontend
432 432 )
433 433 self.add_menu_action(self.edit_menu, self.cut_action, True)
434 434
435 435 self.copy_action = QtGui.QAction("&Copy",
436 436 self,
437 437 shortcut=QtGui.QKeySequence.Copy,
438 438 triggered=self.copy_active_frontend
439 439 )
440 440 self.add_menu_action(self.edit_menu, self.copy_action, True)
441 441
442 442 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
443 443 self,
444 444 shortcut="Ctrl+Shift+C",
445 445 triggered=self.copy_raw_active_frontend
446 446 )
447 447 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
448 448
449 449 self.paste_action = QtGui.QAction("&Paste",
450 450 self,
451 451 shortcut=QtGui.QKeySequence.Paste,
452 452 triggered=self.paste_active_frontend
453 453 )
454 454 self.add_menu_action(self.edit_menu, self.paste_action, True)
455 455
456 456 self.edit_menu.addSeparator()
457 457
458 458 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
459 459 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
460 460 # Only override the default if there is a collision.
461 461 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
462 462 selectall = "Ctrl+Shift+A"
463 463 self.select_all_action = QtGui.QAction("Select &All",
464 464 self,
465 465 shortcut=selectall,
466 466 triggered=self.select_all_active_frontend
467 467 )
468 468 self.add_menu_action(self.edit_menu, self.select_all_action, True)
469 469
470 470
471 471 def init_view_menu(self):
472 472 self.view_menu = self.menuBar().addMenu("&View")
473 473
474 474 if sys.platform != 'darwin':
475 475 # disable on OSX, where there is always a menu bar
476 476 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
477 477 self,
478 478 shortcut="Ctrl+Shift+M",
479 479 statusTip="Toggle visibility of menubar",
480 480 triggered=self.toggle_menu_bar)
481 481 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
482 482
483 483 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
484 484 self.full_screen_act = QtGui.QAction("&Full Screen",
485 485 self,
486 486 shortcut=fs_key,
487 487 statusTip="Toggle between Fullscreen and Normal Size",
488 488 triggered=self.toggleFullScreen)
489 489 self.add_menu_action(self.view_menu, self.full_screen_act)
490 490
491 491 self.view_menu.addSeparator()
492 492
493 493 self.increase_font_size = QtGui.QAction("Zoom &In",
494 494 self,
495 495 shortcut=QtGui.QKeySequence.ZoomIn,
496 496 triggered=self.increase_font_size_active_frontend
497 497 )
498 498 self.add_menu_action(self.view_menu, self.increase_font_size, True)
499 499
500 500 self.decrease_font_size = QtGui.QAction("Zoom &Out",
501 501 self,
502 502 shortcut=QtGui.QKeySequence.ZoomOut,
503 503 triggered=self.decrease_font_size_active_frontend
504 504 )
505 505 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
506 506
507 507 self.reset_font_size = QtGui.QAction("Zoom &Reset",
508 508 self,
509 509 shortcut="Ctrl+0",
510 510 triggered=self.reset_font_size_active_frontend
511 511 )
512 512 self.add_menu_action(self.view_menu, self.reset_font_size, True)
513 513
514 514 self.view_menu.addSeparator()
515 515
516 516 self.clear_action = QtGui.QAction("&Clear Screen",
517 517 self,
518 518 shortcut='Ctrl+L',
519 519 statusTip="Clear the console",
520 520 triggered=self.clear_magic_active_frontend)
521 521 self.add_menu_action(self.view_menu, self.clear_action)
522 522
523 523 def init_kernel_menu(self):
524 524 self.kernel_menu = self.menuBar().addMenu("&Kernel")
525 525 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
526 526 # keep the signal shortcuts to ctrl, rather than
527 527 # platform-default like we do elsewhere.
528 528
529 529 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
530 530
531 531 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
532 532 self,
533 533 triggered=self.interrupt_kernel_active_frontend,
534 534 shortcut=ctrl+"+C",
535 535 )
536 536 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
537 537
538 538 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
539 539 self,
540 540 triggered=self.restart_kernel_active_frontend,
541 541 shortcut=ctrl+"+.",
542 542 )
543 543 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
544 544
545 545 self.kernel_menu.addSeparator()
546 546
547 def _make_dynamic_magic(self,magic):
548 """Return a function `fun` that will execute `magic` on active frontend.
549
550 `magic` : valid python string
551
552 return `fun`, function with no parameters
553
554 `fun` execute `magic` an active frontend at the moment it is triggerd,
555 not the active frontend at the moment it has been created.
556
557 This function is mostly used to create the "All Magics..." Menu at run time.
558 """
559 # need to level nested function to be sure to past magic
560 # on active frontend **at run time**.
561 def inner_dynamic_magic():
562 self.active_frontend.execute(magic)
563 inner_dynamic_magic.__name__ = "dynamics_magic_s"
564 return inner_dynamic_magic
565
566 def populate_all_magic_menu(self, listofmagic=None):
567 """Clean "All Magics..." menu and repopulate it with `listofmagic`
568
569 `listofmagic` : string, repr() of a list of strings.
570
571 `listofmagic`is a repr() of list because it is fed with the result of
572 a 'user_expression'
573 """
574 alm_magic_menu = self.all_magic_menu
575 alm_magic_menu.clear()
576
577 # list of protected magic that don't like to be called without argument
578 # append '?' to the end to print the docstring when called from the menu
579 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
580
581 for magic in eval(listofmagic):
582 if magic in protected_magic:
583 pmagic = '%s%s%s'%('%',magic,'?')
584 else:
585 pmagic = '%s%s'%('%',magic)
586 xaction = QtGui.QAction(pmagic,
587 self,
588 triggered=self._make_dynamic_magic(pmagic)
589 )
590 alm_magic_menu.addAction(xaction)
591
547 592 def update_all_magic_menu(self):
548 593 # first define a callback which will get the list of all magic and put it in the menu.
549 def populate_all_magic_menu(val=None):
550 alm_magic_menu = self.all_magic_menu
551 alm_magic_menu.clear()
552 def make_dynamic_magic(i):
553 def inner_dynamic_magic():
554 self.active_frontend.execute(i)
555 inner_dynamic_magic.__name__ = "dynamics_magic_s"
556 return inner_dynamic_magic
557
558 # list of protected magic that don't like to be called without argument
559 # append '?' to the end to print the docstring when called from the menu
560 protected_magic= ["more","less","load_ext","pycat","loadpy","save"]
561
562 for magic in eval(val):
563 if magic in protected_magic:
564 pmagic = '%s%s%s'%('%',magic,'?')
565 else:
566 pmagic = '%s%s'%('%',magic)
567 xaction = QtGui.QAction(pmagic,
568 self,
569 triggered=make_dynamic_magic(pmagic)
570 )
571 alm_magic_menu.addAction(xaction)
572 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()',populate_all_magic_menu)
594 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()',self.populate_all_magic_menu)
573 595
574 596 def init_magic_menu(self):
575 597 self.magic_menu = self.menuBar().addMenu("&Magic")
576 598 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
577 599
578 600 # this action should not appear as it will be cleard when menu
579 601 # will be updated at first kernel response.
580 602 self.pop = QtGui.QAction("&Update All Magic Menu ",
581 self,
582 triggered=self.update_all_magic_menu)
603 self, triggered=self.update_all_magic_menu)
583 604 self.add_menu_action(self.all_magic_menu, self.pop)
584 605
585 606 self.reset_action = QtGui.QAction("&Reset",
586 607 self,
587 608 statusTip="Clear all varible from workspace",
588 609 triggered=self.reset_magic_active_frontend)
589 610 self.add_menu_action(self.magic_menu, self.reset_action)
590 611
591 612 self.history_action = QtGui.QAction("&History",
592 613 self,
593 614 statusTip="show command history",
594 615 triggered=self.history_magic_active_frontend)
595 616 self.add_menu_action(self.magic_menu, self.history_action)
596 617
597 618 self.save_action = QtGui.QAction("E&xport History ",
598 619 self,
599 620 statusTip="Export History as Python File",
600 621 triggered=self.save_magic_active_frontend)
601 622 self.add_menu_action(self.magic_menu, self.save_action)
602 623
603 624 self.who_action = QtGui.QAction("&Who",
604 625 self,
605 626 statusTip="List interactive variable",
606 627 triggered=self.who_magic_active_frontend)
607 628 self.add_menu_action(self.magic_menu, self.who_action)
608 629
609 630 self.who_ls_action = QtGui.QAction("Wh&o ls",
610 631 self,
611 632 statusTip="Return a list of interactive variable",
612 633 triggered=self.who_ls_magic_active_frontend)
613 634 self.add_menu_action(self.magic_menu, self.who_ls_action)
614 635
615 636 self.whos_action = QtGui.QAction("Who&s",
616 637 self,
617 638 statusTip="List interactive variable with detail",
618 639 triggered=self.whos_magic_active_frontend)
619 640 self.add_menu_action(self.magic_menu, self.whos_action)
620 641
621 642 def init_window_menu(self):
622 643 self.window_menu = self.menuBar().addMenu("&Window")
623 644 if sys.platform == 'darwin':
624 645 # add min/maximize actions to OSX, which lacks default bindings.
625 646 self.minimizeAct = QtGui.QAction("Mini&mize",
626 647 self,
627 648 shortcut="Ctrl+m",
628 649 statusTip="Minimize the window/Restore Normal Size",
629 650 triggered=self.toggleMinimized)
630 651 # maximize is called 'Zoom' on OSX for some reason
631 652 self.maximizeAct = QtGui.QAction("&Zoom",
632 653 self,
633 654 shortcut="Ctrl+Shift+M",
634 655 statusTip="Maximize the window/Restore Normal Size",
635 656 triggered=self.toggleMaximized)
636 657
637 658 self.add_menu_action(self.window_menu, self.minimizeAct)
638 659 self.add_menu_action(self.window_menu, self.maximizeAct)
639 660 self.window_menu.addSeparator()
640 661
641 662 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
642 663 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
643 664 self,
644 665 shortcut=prev_key,
645 666 statusTip="Select previous tab",
646 667 triggered=self.prev_tab)
647 668 self.add_menu_action(self.window_menu, self.prev_tab_act)
648 669
649 670 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
650 671 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
651 672 self,
652 673 shortcut=next_key,
653 674 statusTip="Select next tab",
654 675 triggered=self.next_tab)
655 676 self.add_menu_action(self.window_menu, self.next_tab_act)
656 677
657 678 def init_help_menu(self):
658 679 # please keep the Help menu in Mac Os even if empty. It will
659 680 # automatically contain a search field to search inside menus and
660 681 # please keep it spelled in English, as long as Qt Doesn't support
661 682 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
662 683 # this search field fonctionality
663 684
664 685 self.help_menu = self.menuBar().addMenu("&Help")
665 686
666 687
667 688 # Help Menu
668 689
669 690 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
670 691 self,
671 692 triggered=self.intro_active_frontend
672 693 )
673 694 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
674 695
675 696 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
676 697 self,
677 698 triggered=self.quickref_active_frontend
678 699 )
679 700 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
680 701
681 702 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
682 703 self,
683 704 triggered=self.guiref_active_frontend
684 705 )
685 706 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
686 707
687 708 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
688 709 self,
689 710 triggered=self._open_online_help)
690 711 self.add_menu_action(self.help_menu, self.onlineHelpAct)
691 712
692 713 # minimize/maximize/fullscreen actions:
693 714
694 715 def toggle_menu_bar(self):
695 716 menu_bar = self.menuBar()
696 717 if menu_bar.isVisible():
697 718 menu_bar.setVisible(False)
698 719 else:
699 720 menu_bar.setVisible(True)
700 721
701 722 def toggleMinimized(self):
702 723 if not self.isMinimized():
703 724 self.showMinimized()
704 725 else:
705 726 self.showNormal()
706 727
707 728 def _open_online_help(self):
708 729 filename="http://ipython.org/ipython-doc/stable/index.html"
709 730 webbrowser.open(filename, new=1, autoraise=True)
710 731
711 732 def toggleMaximized(self):
712 733 if not self.isMaximized():
713 734 self.showMaximized()
714 735 else:
715 736 self.showNormal()
716 737
717 738 # Min/Max imizing while in full screen give a bug
718 739 # when going out of full screen, at least on OSX
719 740 def toggleFullScreen(self):
720 741 if not self.isFullScreen():
721 742 self.showFullScreen()
722 743 if sys.platform == 'darwin':
723 744 self.maximizeAct.setEnabled(False)
724 745 self.minimizeAct.setEnabled(False)
725 746 else:
726 747 self.showNormal()
727 748 if sys.platform == 'darwin':
728 749 self.maximizeAct.setEnabled(True)
729 750 self.minimizeAct.setEnabled(True)
730 751
731 752 def close_active_frontend(self):
732 753 self.close_tab(self.active_frontend)
733 754
734 755 def restart_kernel_active_frontend(self):
735 756 self.active_frontend.request_restart_kernel()
736 757
737 758 def interrupt_kernel_active_frontend(self):
738 759 self.active_frontend.request_interrupt_kernel()
739 760
740 761 def cut_active_frontend(self):
741 762 widget = self.active_frontend
742 763 if widget.can_cut():
743 764 widget.cut()
744 765
745 766 def copy_active_frontend(self):
746 767 widget = self.active_frontend
747 768 if widget.can_copy():
748 769 widget.copy()
749 770
750 771 def copy_raw_active_frontend(self):
751 772 self.active_frontend._copy_raw_action.trigger()
752 773
753 774 def paste_active_frontend(self):
754 775 widget = self.active_frontend
755 776 if widget.can_paste():
756 777 widget.paste()
757 778
758 779 def undo_active_frontend(self):
759 780 self.active_frontend.undo()
760 781
761 782 def redo_active_frontend(self):
762 783 self.active_frontend.redo()
763 784
764 785 def reset_magic_active_frontend(self):
765 786 self.active_frontend.execute("%reset")
766 787
767 788 def history_magic_active_frontend(self):
768 789 self.active_frontend.execute("%history")
769 790
770 791 def save_magic_active_frontend(self):
771 792 self.active_frontend.save_magic()
772 793
773 794 def clear_magic_active_frontend(self):
774 795 self.active_frontend.execute("%clear")
775 796
776 797 def who_magic_active_frontend(self):
777 798 self.active_frontend.execute("%who")
778 799
779 800 def who_ls_magic_active_frontend(self):
780 801 self.active_frontend.execute("%who_ls")
781 802
782 803 def whos_magic_active_frontend(self):
783 804 self.active_frontend.execute("%whos")
784 805
785 806 def print_action_active_frontend(self):
786 807 self.active_frontend.print_action.trigger()
787 808
788 809 def export_action_active_frontend(self):
789 810 self.active_frontend.export_action.trigger()
790 811
791 812 def select_all_active_frontend(self):
792 813 self.active_frontend.select_all_action.trigger()
793 814
794 815 def increase_font_size_active_frontend(self):
795 816 self.active_frontend.increase_font_size.trigger()
796 817
797 818 def decrease_font_size_active_frontend(self):
798 819 self.active_frontend.decrease_font_size.trigger()
799 820
800 821 def reset_font_size_active_frontend(self):
801 822 self.active_frontend.reset_font_size.trigger()
802 823
803 824 def guiref_active_frontend(self):
804 825 self.active_frontend.execute("%guiref")
805 826
806 827 def intro_active_frontend(self):
807 828 self.active_frontend.execute("?")
808 829
809 830 def quickref_active_frontend(self):
810 831 self.active_frontend.execute("%quickref")
811 832 #---------------------------------------------------------------------------
812 833 # QWidget interface
813 834 #---------------------------------------------------------------------------
814 835
815 836 def closeEvent(self, event):
816 837 """ Forward the close event to every tabs contained by the windows
817 838 """
818 839 if self.tab_widget.count() == 0:
819 840 # no tabs, just close
820 841 event.accept()
821 842 return
822 843 # Do Not loop on the widget count as it change while closing
823 844 title = self.window().windowTitle()
824 845 cancel = QtGui.QMessageBox.Cancel
825 846 okay = QtGui.QMessageBox.Ok
826 847
827 848 if self.confirm_exit:
828 849 if self.tab_widget.count() > 1:
829 850 msg = "Close all tabs, stop all kernels, and Quit?"
830 851 else:
831 852 msg = "Close console, stop kernel, and Quit?"
832 853 info = "Kernels not started here (e.g. notebooks) will be left alone."
833 854 closeall = QtGui.QPushButton("&Yes, quit everything", self)
834 855 closeall.setShortcut('Y')
835 856 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
836 857 title, msg)
837 858 box.setInformativeText(info)
838 859 box.addButton(cancel)
839 860 box.addButton(closeall, QtGui.QMessageBox.YesRole)
840 861 box.setDefaultButton(closeall)
841 862 box.setEscapeButton(cancel)
842 863 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
843 864 box.setIconPixmap(pixmap)
844 865 reply = box.exec_()
845 866 else:
846 867 reply = okay
847 868
848 869 if reply == cancel:
849 870 event.ignore()
850 871 return
851 872 if reply == okay:
852 873 while self.tab_widget.count() >= 1:
853 874 # prevent further confirmations:
854 875 widget = self.active_frontend
855 876 widget._confirm_exit = False
856 877 self.close_tab(widget)
857 878 event.accept()
858 879
@@ -1,552 +1,552 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import json
23 23 import os
24 24 import signal
25 25 import sys
26 26 import uuid
27 27
28 28 # System library imports
29 29 from IPython.external.qt import QtGui
30 30
31 31 # Local imports
32 32 from IPython.config.application import boolean_flag, catch_config_error
33 33 from IPython.core.application import BaseIPythonApplication
34 34 from IPython.core.profiledir import ProfileDir
35 35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 39 from IPython.frontend.qt.console import styles
40 40 from IPython.frontend.qt.console.mainwindow import MainWindow
41 41 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 42 from IPython.utils.path import filefind
43 43 from IPython.utils.py3compat import str_to_bytes
44 44 from IPython.utils.traitlets import (
45 45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 46 )
47 47 from IPython.zmq.ipkernel import (
48 48 flags as ipkernel_flags,
49 49 aliases as ipkernel_aliases,
50 50 IPKernelApp
51 51 )
52 52 from IPython.zmq.session import Session, default_secure
53 53 from IPython.zmq.zmqshell import ZMQInteractiveShell
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Network Constants
57 57 #-----------------------------------------------------------------------------
58 58
59 59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Globals
63 63 #-----------------------------------------------------------------------------
64 64
65 65 _examples = """
66 66 ipython qtconsole # start the qtconsole
67 67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 68 """
69 69
70 70 #-----------------------------------------------------------------------------
71 71 # Aliases and Flags
72 72 #-----------------------------------------------------------------------------
73 73
74 74 flags = dict(ipkernel_flags)
75 75 qt_flags = {
76 76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
77 77 "Connect to an existing kernel. If no argument specified, guess most recent"),
78 78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 79 "Use a pure Python kernel instead of an IPython kernel."),
80 80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 81 "Disable rich text support."),
82 82 }
83 83 qt_flags.update(boolean_flag(
84 84 'gui-completion', 'ConsoleWidget.gui_completion',
85 85 "use a GUI widget for tab completion",
86 86 "use plaintext output for completion"
87 87 ))
88 88 qt_flags.update(boolean_flag(
89 89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
90 90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
91 91 to force a direct exit without any confirmation.
92 92 """,
93 93 """Don't prompt the user when exiting. This will terminate the kernel
94 94 if it is owned by the frontend, and leave it alive if it is external.
95 95 """
96 96 ))
97 97 flags.update(qt_flags)
98 98
99 99 aliases = dict(ipkernel_aliases)
100 100
101 101 qt_aliases = dict(
102 102 hb = 'IPythonQtConsoleApp.hb_port',
103 103 shell = 'IPythonQtConsoleApp.shell_port',
104 104 iopub = 'IPythonQtConsoleApp.iopub_port',
105 105 stdin = 'IPythonQtConsoleApp.stdin_port',
106 106 ip = 'IPythonQtConsoleApp.ip',
107 107 existing = 'IPythonQtConsoleApp.existing',
108 108 f = 'IPythonQtConsoleApp.connection_file',
109 109
110 110 style = 'IPythonWidget.syntax_style',
111 111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 112 colors = 'ZMQInteractiveShell.colors',
113 113
114 114 editor = 'IPythonWidget.editor',
115 115 paging = 'ConsoleWidget.paging',
116 116 ssh = 'IPythonQtConsoleApp.sshserver',
117 117 )
118 118 aliases.update(qt_aliases)
119 119
120 120 #-----------------------------------------------------------------------------
121 121 # Classes
122 122 #-----------------------------------------------------------------------------
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # IPythonQtConsole
126 126 #-----------------------------------------------------------------------------
127 127
128 128
129 129 class IPythonQtConsoleApp(BaseIPythonApplication):
130 130 name = 'ipython-qtconsole'
131 131 default_config_file_name='ipython_config.py'
132 132
133 133 description = """
134 134 The IPython QtConsole.
135 135
136 136 This launches a Console-style application using Qt. It is not a full
137 137 console, in that launched terminal subprocesses will not be able to accept
138 138 input.
139 139
140 140 The QtConsole supports various extra features beyond the Terminal IPython
141 141 shell, such as inline plotting with matplotlib, via:
142 142
143 143 ipython qtconsole --pylab=inline
144 144
145 145 as well as saving your session as HTML, and printing the output.
146 146
147 147 """
148 148 examples = _examples
149 149
150 150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
151 151 flags = Dict(flags)
152 152 aliases = Dict(aliases)
153 153
154 154 kernel_argv = List(Unicode)
155 155
156 156 # create requested profiles by default, if they don't exist:
157 157 auto_create = CBool(True)
158 158 # connection info:
159 159 ip = Unicode(LOCALHOST, config=True,
160 160 help="""Set the kernel\'s IP address [default localhost].
161 161 If the IP address is something other than localhost, then
162 162 Consoles on other machines will be able to connect
163 163 to the Kernel, so be careful!"""
164 164 )
165 165
166 166 sshserver = Unicode('', config=True,
167 167 help="""The SSH server to use to connect to the kernel.""")
168 168 sshkey = Unicode('', config=True,
169 169 help="""Path to the ssh key to use for logging in to the ssh server.""")
170 170
171 171 hb_port = Integer(0, config=True,
172 172 help="set the heartbeat port [default: random]")
173 173 shell_port = Integer(0, config=True,
174 174 help="set the shell (XREP) port [default: random]")
175 175 iopub_port = Integer(0, config=True,
176 176 help="set the iopub (PUB) port [default: random]")
177 177 stdin_port = Integer(0, config=True,
178 178 help="set the stdin (XREQ) port [default: random]")
179 179 connection_file = Unicode('', config=True,
180 180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
181 181
182 182 This file will contain the IP, ports, and authentication key needed to connect
183 183 clients to this kernel. By default, this file will be created in the security-dir
184 184 of the current profile, but can be specified by absolute path.
185 185 """)
186 186 def _connection_file_default(self):
187 187 return 'kernel-%i.json' % os.getpid()
188 188
189 189 existing = Unicode('', config=True,
190 190 help="""Connect to an already running kernel""")
191 191
192 192 stylesheet = Unicode('', config=True,
193 193 help="path to a custom CSS stylesheet")
194 194
195 195 pure = CBool(False, config=True,
196 196 help="Use a pure Python kernel instead of an IPython kernel.")
197 197 plain = CBool(False, config=True,
198 198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
199 199
200 200 def _pure_changed(self, name, old, new):
201 201 kind = 'plain' if self.plain else 'rich'
202 202 self.config.ConsoleWidget.kind = kind
203 203 if self.pure:
204 204 self.widget_factory = FrontendWidget
205 205 elif self.plain:
206 206 self.widget_factory = IPythonWidget
207 207 else:
208 208 self.widget_factory = RichIPythonWidget
209 209
210 210 _plain_changed = _pure_changed
211 211
212 212 confirm_exit = CBool(True, config=True,
213 213 help="""
214 214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
215 215 to force a direct exit without any confirmation.""",
216 216 )
217 217
218 218 # the factory for creating a widget
219 219 widget_factory = Any(RichIPythonWidget)
220 220
221 221 def parse_command_line(self, argv=None):
222 222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
223 223 if argv is None:
224 224 argv = sys.argv[1:]
225 225 self.kernel_argv = list(argv) # copy
226 226 # kernel should inherit default config file from frontend
227 227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
228 228 # Scrub frontend-specific flags
229 229 swallow_next = False
230 230 was_flag = False
231 231 # copy again, in case some aliases have the same name as a flag
232 232 # argv = list(self.kernel_argv)
233 233 for a in argv:
234 234 if swallow_next:
235 235 swallow_next = False
236 236 # last arg was an alias, remove the next one
237 237 # *unless* the last alias has a no-arg flag version, in which
238 238 # case, don't swallow the next arg if it's also a flag:
239 239 if not (was_flag and a.startswith('-')):
240 240 self.kernel_argv.remove(a)
241 241 continue
242 242 if a.startswith('-'):
243 243 split = a.lstrip('-').split('=')
244 244 alias = split[0]
245 245 if alias in qt_aliases:
246 246 self.kernel_argv.remove(a)
247 247 if len(split) == 1:
248 248 # alias passed with arg via space
249 249 swallow_next = True
250 250 # could have been a flag that matches an alias, e.g. `existing`
251 251 # in which case, we might not swallow the next arg
252 252 was_flag = alias in qt_flags
253 253 elif alias in qt_flags:
254 254 # strip flag, but don't swallow next, as flags don't take args
255 255 self.kernel_argv.remove(a)
256 256
257 257 def init_connection_file(self):
258 258 """find the connection file, and load the info if found.
259 259
260 260 The current working directory and the current profile's security
261 261 directory will be searched for the file if it is not given by
262 262 absolute path.
263 263
264 264 When attempting to connect to an existing kernel and the `--existing`
265 265 argument does not match an existing file, it will be interpreted as a
266 266 fileglob, and the matching file in the current profile's security dir
267 267 with the latest access time will be used.
268 268 """
269 269 if self.existing:
270 270 try:
271 271 cf = find_connection_file(self.existing)
272 272 except Exception:
273 273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
274 274 self.exit(1)
275 275 self.log.info("Connecting to existing kernel: %s" % cf)
276 276 self.connection_file = cf
277 277 # should load_connection_file only be used for existing?
278 278 # as it is now, this allows reusing ports if an existing
279 279 # file is requested
280 280 try:
281 281 self.load_connection_file()
282 282 except Exception:
283 283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
284 284 self.exit(1)
285 285
286 286 def load_connection_file(self):
287 287 """load ip/port/hmac config from JSON connection file"""
288 288 # this is identical to KernelApp.load_connection_file
289 289 # perhaps it can be centralized somewhere?
290 290 try:
291 291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
292 292 except IOError:
293 293 self.log.debug("Connection File not found: %s", self.connection_file)
294 294 return
295 295 self.log.debug(u"Loading connection file %s", fname)
296 296 with open(fname) as f:
297 297 s = f.read()
298 298 cfg = json.loads(s)
299 299 if self.ip == LOCALHOST and 'ip' in cfg:
300 300 # not overridden by config or cl_args
301 301 self.ip = cfg['ip']
302 302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
303 303 name = channel + '_port'
304 304 if getattr(self, name) == 0 and name in cfg:
305 305 # not overridden by config or cl_args
306 306 setattr(self, name, cfg[name])
307 307 if 'key' in cfg:
308 308 self.config.Session.key = str_to_bytes(cfg['key'])
309 309
310 310 def init_ssh(self):
311 311 """set up ssh tunnels, if needed."""
312 312 if not self.sshserver and not self.sshkey:
313 313 return
314 314
315 315 if self.sshkey and not self.sshserver:
316 316 # specifying just the key implies that we are connecting directly
317 317 self.sshserver = self.ip
318 318 self.ip = LOCALHOST
319 319
320 320 # build connection dict for tunnels:
321 321 info = dict(ip=self.ip,
322 322 shell_port=self.shell_port,
323 323 iopub_port=self.iopub_port,
324 324 stdin_port=self.stdin_port,
325 325 hb_port=self.hb_port
326 326 )
327 327
328 328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
329 329
330 330 # tunnels return a new set of ports, which will be on localhost:
331 331 self.ip = LOCALHOST
332 332 try:
333 333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
334 334 except:
335 335 # even catch KeyboardInterrupt
336 336 self.log.error("Could not setup tunnels", exc_info=True)
337 337 self.exit(1)
338 338
339 339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
340 340
341 341 cf = self.connection_file
342 342 base,ext = os.path.splitext(cf)
343 343 base = os.path.basename(base)
344 344 self.connection_file = os.path.basename(base)+'-ssh'+ext
345 345 self.log.critical("To connect another client via this tunnel, use:")
346 346 self.log.critical("--existing %s" % self.connection_file)
347 347
348 348 def _new_connection_file(self):
349 349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
350 350
351 351 def init_kernel_manager(self):
352 352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
353 353 signal.signal(signal.SIGINT, signal.SIG_DFL)
354 354 sec = self.profile_dir.security_dir
355 355 try:
356 356 cf = filefind(self.connection_file, ['.', sec])
357 357 except IOError:
358 358 # file might not exist
359 359 if self.connection_file == os.path.basename(self.connection_file):
360 360 # just shortname, put it in security dir
361 361 cf = os.path.join(sec, self.connection_file)
362 362 else:
363 363 cf = self.connection_file
364 364
365 365 # Create a KernelManager and start a kernel.
366 366 self.kernel_manager = QtKernelManager(
367 367 ip=self.ip,
368 368 shell_port=self.shell_port,
369 369 iopub_port=self.iopub_port,
370 370 stdin_port=self.stdin_port,
371 371 hb_port=self.hb_port,
372 372 connection_file=cf,
373 373 config=self.config,
374 374 )
375 375 # start the kernel
376 376 if not self.existing:
377 377 kwargs = dict(ipython=not self.pure)
378 378 kwargs['extra_arguments'] = self.kernel_argv
379 379 self.kernel_manager.start_kernel(**kwargs)
380 380 elif self.sshserver:
381 381 # ssh, write new connection file
382 382 self.kernel_manager.write_connection_file()
383 383 self.kernel_manager.start_channels()
384 384
385 385 def new_frontend_master(self):
386 386 """ Create and return new frontend attached to new kernel, launched on localhost.
387 387 """
388 388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
389 389 kernel_manager = QtKernelManager(
390 390 ip=ip,
391 391 connection_file=self._new_connection_file(),
392 392 config=self.config,
393 393 )
394 394 # start the kernel
395 395 kwargs = dict(ipython=not self.pure)
396 396 kwargs['extra_arguments'] = self.kernel_argv
397 397 kernel_manager.start_kernel(**kwargs)
398 398 kernel_manager.start_channels()
399 399 widget = self.widget_factory(config=self.config,
400 400 local_kernel=True)
401 401 widget.kernel_manager = kernel_manager
402 402 widget._existing = False
403 403 widget._may_close = True
404 404 widget._confirm_exit = self.confirm_exit
405 405 return widget
406 406
407 407 def new_frontend_slave(self, current_widget):
408 408 """Create and return a new frontend attached to an existing kernel.
409 409
410 410 Parameters
411 411 ----------
412 412 current_widget : IPythonWidget
413 413 The IPythonWidget whose kernel this frontend is to share
414 414 """
415 415 kernel_manager = QtKernelManager(
416 416 connection_file=current_widget.kernel_manager.connection_file,
417 417 config = self.config,
418 418 )
419 419 kernel_manager.load_connection_file()
420 420 kernel_manager.start_channels()
421 421 widget = self.widget_factory(config=self.config,
422 422 local_kernel=False)
423 423 widget._existing = True
424 424 widget._may_close = False
425 425 widget._confirm_exit = False
426 426 widget.kernel_manager = kernel_manager
427 427 return widget
428 428
429 429 def init_qt_elements(self):
430 430 # Create the widget.
431 431 self.app = QtGui.QApplication([])
432 432
433 433 base_path = os.path.abspath(os.path.dirname(__file__))
434 434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
435 435 self.app.icon = QtGui.QIcon(icon_path)
436 436 QtGui.QApplication.setWindowIcon(self.app.icon)
437 437
438 438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
439 439 self.widget = self.widget_factory(config=self.config,
440 440 local_kernel=local_kernel)
441 441 self.widget._existing = self.existing
442 442 self.widget._may_close = not self.existing
443 443 self.widget._confirm_exit = self.confirm_exit
444 444
445 445 self.widget.kernel_manager = self.kernel_manager
446 446 self.window = MainWindow(self.app,
447 447 confirm_exit=self.confirm_exit,
448 448 new_frontend_factory=self.new_frontend_master,
449 449 slave_frontend_factory=self.new_frontend_slave,
450 450 )
451 451 self.window.log = self.log
452 452 self.window.add_tab_with_frontend(self.widget)
453 453 self.window.init_menu_bar()
454 454
455 #we need to populate the 'Magic Menu' once the kernel has answer at least once
455 # we need to populate the 'Magic Menu' once the kernel has answer at least once
456 456 self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
457 457
458 458 self.window.setWindowTitle('Python' if self.pure else 'IPython')
459 459
460 460 def init_colors(self):
461 461 """Configure the coloring of the widget"""
462 462 # Note: This will be dramatically simplified when colors
463 463 # are removed from the backend.
464 464
465 465 if self.pure:
466 466 # only IPythonWidget supports styling
467 467 return
468 468
469 469 # parse the colors arg down to current known labels
470 470 try:
471 471 colors = self.config.ZMQInteractiveShell.colors
472 472 except AttributeError:
473 473 colors = None
474 474 try:
475 475 style = self.config.IPythonWidget.syntax_style
476 476 except AttributeError:
477 477 style = None
478 478
479 479 # find the value for colors:
480 480 if colors:
481 481 colors=colors.lower()
482 482 if colors in ('lightbg', 'light'):
483 483 colors='lightbg'
484 484 elif colors in ('dark', 'linux'):
485 485 colors='linux'
486 486 else:
487 487 colors='nocolor'
488 488 elif style:
489 489 if style=='bw':
490 490 colors='nocolor'
491 491 elif styles.dark_style(style):
492 492 colors='linux'
493 493 else:
494 494 colors='lightbg'
495 495 else:
496 496 colors=None
497 497
498 498 # Configure the style.
499 499 widget = self.widget
500 500 if style:
501 501 widget.style_sheet = styles.sheet_from_template(style, colors)
502 502 widget.syntax_style = style
503 503 widget._syntax_style_changed()
504 504 widget._style_sheet_changed()
505 505 elif colors:
506 506 # use a default style
507 507 widget.set_default_style(colors=colors)
508 508 else:
509 509 # this is redundant for now, but allows the widget's
510 510 # defaults to change
511 511 widget.set_default_style()
512 512
513 513 if self.stylesheet:
514 514 # we got an expicit stylesheet
515 515 if os.path.isfile(self.stylesheet):
516 516 with open(self.stylesheet) as f:
517 517 sheet = f.read()
518 518 widget.style_sheet = sheet
519 519 widget._style_sheet_changed()
520 520 else:
521 521 raise IOError("Stylesheet %r not found."%self.stylesheet)
522 522
523 523 @catch_config_error
524 524 def initialize(self, argv=None):
525 525 super(IPythonQtConsoleApp, self).initialize(argv)
526 526 self.init_connection_file()
527 527 default_secure(self.config)
528 528 self.init_ssh()
529 529 self.init_kernel_manager()
530 530 self.init_qt_elements()
531 531 self.init_colors()
532 532
533 533 def start(self):
534 534
535 535 # draw the window
536 536 self.window.show()
537 537
538 538 # Start the application main loop.
539 539 self.app.exec_()
540 540
541 541 #-----------------------------------------------------------------------------
542 542 # Main entry point
543 543 #-----------------------------------------------------------------------------
544 544
545 545 def main():
546 546 app = IPythonQtConsoleApp()
547 547 app.initialize()
548 548 app.start()
549 549
550 550
551 551 if __name__ == '__main__':
552 552 main()
General Comments 0
You need to be logged in to leave comments. Login now