1
# -*- coding: utf-8 -*-
3
# Copyright © 2012 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
7
"""IPython Console plugin
9
Handles IPython clients (and in the future, will handle IPython kernels too
11
# pylint: disable=C0103
12
# pylint: disable=R0903
13
# pylint: disable=R0911
14
# pylint: disable=R0201
16
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QWidget, QGroupBox,
17
QLineEdit, QInputDialog, QTabWidget, QMenu,
18
QFontComboBox, QHBoxLayout, QApplication,
19
QToolButton, QLabel, QKeySequence)
20
from spyderlib.qt.QtCore import SIGNAL, Qt, QUrl
28
from IPython.config.loader import Config, load_pyconfig_files
29
from IPython.core.application import get_ipython_dir
32
from spyderlib.baseconfig import get_conf_path, _
33
from spyderlib.guiconfig import get_icon
34
from spyderlib.utils import programs
35
from spyderlib.utils.misc import get_error_match
36
from spyderlib.utils.qthelpers import (create_action, create_toolbutton,
37
add_actions, get_std_icon)
38
from spyderlib.widgets.tabs import Tabs
39
from spyderlib.widgets.ipython import IPythonApp
40
from spyderlib.widgets.findreplace import FindReplace
41
from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage
42
from spyderlib.widgets.sourcecode import mixins
45
class IPythonConsoleConfigPage(PluginConfigPage):
46
def __init__(self, plugin, parent):
47
PluginConfigPage.__init__(self, plugin, parent)
48
self.get_name = lambda: _("IPython console")
51
newcb = self.create_checkbox
52
mpl_present = programs.is_module_installed("matplotlib")
55
font_group = self.create_fontgroup(option=None, text=None,
56
fontfilters=QFontComboBox.MonospacedFonts)
59
interface_group = QGroupBox(_("Interface"))
60
banner_box = newcb(_("Display initial banner"), 'show_banner',
61
tip=_("This option lets you hide the message shown at\n"
62
"the top of the console when it's opened."))
63
gui_comp_box = newcb(_("Use a completion widget"),
65
tip=_("Use a widget instead of plain text "
66
"output for tab completion"))
67
pager_box = newcb(_("Use a pager to display additional text inside "
68
"the console"), 'use_pager',
69
tip=_("Useful if you don't want to fill the "
70
"console with long help or completion texts.\n"
71
"Note: Use the Q key to get out of the "
73
calltips_box = newcb(_("Display balloon tips"), 'show_calltips')
74
ask_box = newcb(_("Ask for confirmation before closing"),
77
interface_layout = QVBoxLayout()
78
interface_layout.addWidget(banner_box)
79
interface_layout.addWidget(gui_comp_box)
80
interface_layout.addWidget(pager_box)
81
interface_layout.addWidget(calltips_box)
82
interface_layout.addWidget(ask_box)
83
interface_group.setLayout(interface_layout)
85
# Background Color Group
86
bg_group = QGroupBox(_("Background color"))
87
light_radio = self.create_radiobutton(_("Light background"),
89
dark_radio = self.create_radiobutton(_("Dark background"),
91
bg_layout = QVBoxLayout()
92
bg_layout.addWidget(light_radio)
93
bg_layout.addWidget(dark_radio)
94
bg_group.setLayout(bg_layout)
97
source_code_group = QGroupBox(_("Source code"))
98
buffer_spin = self.create_spinbox(
99
_("Buffer: "), _(" lines"),
100
'buffer_size', min_=-1, max_=1000000, step=100,
101
tip=_("Set the maximum number of lines of text shown in the\n"
102
"console before truncation. Specifying -1 disables it\n"
103
"(not recommended!)"))
104
source_code_layout = QVBoxLayout()
105
source_code_layout.addWidget(buffer_spin)
106
source_code_group.setLayout(source_code_layout)
110
pylab_group = QGroupBox(_("Support for graphics (Pylab)"))
111
pylab_box = newcb(_("Activate support"), 'pylab')
112
autoload_pylab_box = newcb(_("Automatically load Pylab and NumPy"),
114
tip=_("This lets you load graphics support "
115
"without importing \nthe commands to do "
116
"plots. Useful to work with other\n"
117
"plotting libraries different to "
118
"Matplotlib or to develop \nGUIs with "
120
autoload_pylab_box.setEnabled(self.get_option('pylab') and mpl_present)
121
self.connect(pylab_box, SIGNAL("toggled(bool)"),
122
autoload_pylab_box.setEnabled)
124
pylab_layout = QVBoxLayout()
125
pylab_layout.addWidget(pylab_box)
126
pylab_layout.addWidget(autoload_pylab_box)
127
pylab_group.setLayout(pylab_layout)
130
self.set_option('pylab', False)
131
self.set_option('pylab/autoload', False)
132
pylab_group.setEnabled(False)
133
pylab_tip = _("This feature requires the Matplotlib library.\n"
134
"It seems you don't have it installed.")
135
pylab_box.setToolTip(pylab_tip)
137
# Pylab backend Group
139
automatic = _("Automatic")
140
backend_group = QGroupBox(_("Graphics backend"))
141
bend_label = QLabel(_("Decide how graphics are going to be displayed "
142
"in the console. If unsure, please select "
143
"<b>%s</b> to put graphics inside the "
144
"console or <b>%s</b> to interact with "
145
"them (through zooming and panning) in a "
146
"separate window.") % (inline, automatic))
147
bend_label.setWordWrap(True)
149
backends = [(inline, 0), (automatic, 1), ("Qt", 2)]
150
# TODO: Add gtk3 when 0.13 is released
151
if sys.platform == 'darwin':
152
backends.append( ("Mac OSX", 3) )
153
if programs.is_module_installed('pygtk'):
154
backends.append( ("Gtk", 4) )
155
if programs.is_module_installed('wxPython'):
156
backends.append( ("Wx", 5) )
157
if programs.is_module_installed('_tkinter'):
158
backends.append( ("Tkinter", 6) )
159
backends = tuple(backends)
161
backend_box = self.create_combobox( _("Backend:")+" ", backends,
162
'pylab/backend', default=0,
163
tip=_("This option will be applied the "
164
"next time a console is opened."))
166
backend_layout = QVBoxLayout()
167
backend_layout.addWidget(bend_label)
168
backend_layout.addWidget(backend_box)
169
backend_group.setLayout(backend_layout)
170
backend_group.setEnabled(self.get_option('pylab') and mpl_present)
171
self.connect(pylab_box, SIGNAL("toggled(bool)"),
172
backend_group.setEnabled)
174
# Inline backend Group
175
inline_group = QGroupBox(_("Inline backend"))
176
inline_label = QLabel(_("Decide how to render the figures created by "
178
inline_label.setWordWrap(True)
179
formats = (("PNG", 0), ("SVG", 1))
180
format_box = self.create_combobox(_("Format:")+" ", formats,
181
'pylab/inline/figure_format', default=0)
182
resolution_spin = self.create_spinbox(
183
_("Resolution:")+" ", " "+_("dpi"),
184
'pylab/inline/resolution', min_=56, max_=112, step=1,
185
tip=_("Only used when the format is PNG. Default is "
187
width_spin = self.create_spinbox(
188
_("Width:")+" ", " "+_("inches"),
189
'pylab/inline/width', min_=4, max_=20, step=1,
190
tip=_("Default is 6"))
191
height_spin = self.create_spinbox(
192
_("Height:")+" ", " "+_("inches"),
193
'pylab/inline/height', min_=4, max_=20, step=1,
194
tip=_("Default is 4"))
196
inline_layout = QVBoxLayout()
197
inline_layout.addWidget(inline_label)
198
inline_layout.addWidget(format_box)
199
inline_layout.addWidget(resolution_spin)
200
inline_layout.addWidget(width_spin)
201
inline_layout.addWidget(height_spin)
202
inline_group.setLayout(inline_layout)
203
inline_group.setEnabled(self.get_option('pylab') and mpl_present)
204
self.connect(pylab_box, SIGNAL("toggled(bool)"),
205
inline_group.setEnabled)
209
run_lines_group = QGroupBox(_("Run code"))
210
run_lines_label = QLabel(_("You can run several lines of code when "
211
"a console is started. Please introduce "
212
"each one separated by commas, for "
214
"<i>import os, import sys</i>"))
215
run_lines_label.setWordWrap(True)
216
run_lines_edit = self.create_lineedit(_("Lines:"), 'startup/run_lines',
217
'', alignment=Qt.Horizontal)
219
run_lines_layout = QVBoxLayout()
220
run_lines_layout.addWidget(run_lines_label)
221
run_lines_layout.addWidget(run_lines_edit)
222
run_lines_group.setLayout(run_lines_layout)
225
run_file_group = QGroupBox(_("Run a file"))
226
run_file_label = QLabel(_("You can also run a whole file at startup "
227
"instead of just some lines (This is "
228
"similar to have a PYTHONSTARTUP file)."))
229
run_file_label.setWordWrap(True)
230
file_radio = newcb(_("Use the following file:"),
231
'startup/use_run_file', False)
232
run_file_browser = self.create_browsefile('', 'startup/run_file', '')
233
run_file_browser.setEnabled(False)
234
self.connect(file_radio, SIGNAL("toggled(bool)"),
235
run_file_browser.setEnabled)
237
run_file_layout = QVBoxLayout()
238
run_file_layout.addWidget(run_file_label)
239
run_file_layout.addWidget(file_radio)
240
run_file_layout.addWidget(run_file_browser)
241
run_file_group.setLayout(run_file_layout)
244
spyder_group = QGroupBox(_("Spyder startup"))
245
ipystartup_box = newcb(_("Open an IPython console at startup"),
246
"open_ipython_at_startup")
247
spyder_layout = QVBoxLayout()
248
spyder_layout.addWidget(ipystartup_box)
249
spyder_group.setLayout(spyder_layout)
251
# ---- Advanced settings ----
252
# Greedy completer group
253
greedy_group = QGroupBox(_("Greedy completion"))
254
greedy_label = QLabel(_("Enable <tt>Tab</tt> completion on elements "
255
"of lists, results of function calls, etc, "
256
"<i>without</i> assigning them to a "
258
"For example, you can get completions on "
259
"things like <tt>li[0].<Tab></tt> or "
260
"<tt>ins.meth().<Tab></tt>"))
261
greedy_label.setWordWrap(True)
262
greedy_box = newcb(_("Use the greedy completer"), "greedy_completer",
263
tip="<b>Warning</b>: It can be unsafe because the "
264
"code is actually evaluated when you press "
267
greedy_layout = QVBoxLayout()
268
greedy_layout.addWidget(greedy_label)
269
greedy_layout.addWidget(greedy_box)
270
greedy_group.setLayout(greedy_layout)
273
autocall_group = QGroupBox(_("Autocall"))
274
autocall_label = QLabel(_("Autocall makes IPython automatically call "
275
"any callable object even if you didn't type "
276
"explicit parentheses.<br>"
277
"For example, if you type <i>str 43</i> it "
278
"becomes <i>str(43)</i> automatically."))
279
autocall_label.setWordWrap(True)
283
autocall_opts = ((_('Off'), 0), (smart, 1), (full, 2))
284
autocall_box = self.create_combobox(
285
_("Autocall: "), autocall_opts, 'autocall', default=0,
286
tip=_("On <b>%s</b> mode, Autocall is not applied if "
287
"there are no arguments after the callable. On "
288
"<b>%s</b> mode, all callable objects are "
289
"automatically called (even if no arguments are "
290
"present).") % (smart, full))
292
autocall_layout = QVBoxLayout()
293
autocall_layout.addWidget(autocall_label)
294
autocall_layout.addWidget(autocall_box)
295
autocall_group.setLayout(autocall_layout)
298
sympy_group = QGroupBox(_("Symbolic Mathematics"))
299
sympy_label = QLabel(_("Perfom symbolic operations in the console "
300
"(e.g. integrals, derivatives, vector calculus, "
301
"etc) and get the outputs in a beautifully "
303
sympy_label.setWordWrap(True)
304
sympy_box = newcb(_("Use symbolic math"), "symbolic_math",
305
tip=_("This option loads the Sympy library to work "
306
"with.<br>Please refer to its documentation to "
307
"learn how to use it."))
309
sympy_layout = QVBoxLayout()
310
sympy_layout.addWidget(sympy_label)
311
sympy_layout.addWidget(sympy_box)
312
sympy_group.setLayout(sympy_layout)
314
sympy_present = programs.is_module_installed("sympy")
315
if not sympy_present:
316
self.set_option("symbolic_math", False)
317
sympy_box.setEnabled(False)
318
sympy_tip = _("This feature requires the Sympy library.\n"
319
"It seems you don't have it installed.")
320
sympy_box.setToolTip(sympy_tip)
323
prompts_group = QGroupBox(_("Prompts"))
324
prompts_label = QLabel(_("Modify how Input and Output prompts are "
325
"shown in the console."))
326
prompts_label.setWordWrap(True)
327
in_prompt_edit = self.create_lineedit(_("Input prompt:"),
330
'In [<span class="in-prompt-number">'
331
'%i</span>]:'),
332
alignment=Qt.Horizontal)
333
out_prompt_edit = self.create_lineedit(_("Output prompt:"),
336
'Out[<span class="out-prompt-number">'
337
'%i</span>]:'),
338
alignment=Qt.Horizontal)
340
prompts_layout = QVBoxLayout()
341
prompts_layout.addWidget(prompts_label)
342
prompts_layout.addWidget(in_prompt_edit)
343
prompts_layout.addWidget(out_prompt_edit)
344
prompts_group.setLayout(prompts_layout)
346
# --- Tabs organization ---
348
tabs.addTab(self.create_tab(font_group, interface_group, bg_group,
349
source_code_group), _("Display"))
350
tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group),
352
tabs.addTab(self.create_tab(spyder_group, run_lines_group,
353
run_file_group), _("Startup"))
354
tabs.addTab(self.create_tab(greedy_group, autocall_group, sympy_group,
355
prompts_group), _("Advanced Settings"))
357
vlayout = QVBoxLayout()
358
vlayout.addWidget(tabs)
359
self.setLayout(vlayout)
362
#XXX: For now, we add this layer to the IPython widget (which is the
363
# `ipython_widget` attribute of this `IPythonClient` class) even if this is
364
# quite featureless: the IPythonClient has a vertical layout which contains
365
# only the IPython widget inside it. So we could have directly made the
366
# IPythonClient class inherit from the IPython widget's class. However,
367
# the latter is not yet clearly defined: IPython API is quite unclear and
368
# confusing for this matter, so I prefered to add this layer. But that's
369
# just a start: we should replace it by the correct inheritance logic in
371
class IPythonClient(QWidget, mixins.SaveHistoryMixin):
372
"""Spyder IPython client (or frontend)"""
374
CONF_SECTION = 'ipython'
375
SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime())
377
def __init__(self, plugin, connection_file, kernel_widget_id, client_name,
378
ipython_widget, history_filename, menu_actions=None):
379
super(IPythonClient, self).__init__(plugin)
380
mixins.SaveHistoryMixin.__init__(self)
381
self.options_button = None
383
self.connection_file = connection_file
384
self.kernel_widget_id = kernel_widget_id
385
self.client_name = client_name
386
self.ipython_widget = ipython_widget
387
self.menu_actions = menu_actions
388
self.history_filename = get_conf_path(history_filename)
391
vlayout = QVBoxLayout()
392
toolbar_buttons = self.get_toolbar_buttons()
393
hlayout = QHBoxLayout()
394
for button in toolbar_buttons:
395
hlayout.addWidget(button)
396
vlayout.addLayout(hlayout)
397
vlayout.setContentsMargins(0, 0, 0, 0)
398
vlayout.addWidget(self.ipython_widget)
399
self.setLayout(vlayout)
401
self.exit_callback = lambda: plugin.close_console(widget=self)
403
# Connect the IPython widget to this IPython client:
404
# (see spyderlib/widgets/ipython.py for more details about this)
405
ipython_widget.set_ipython_client(self)
408
self.ipython_widget.executing.connect(
409
lambda c: self.add_to_history(command=c))
411
# To update history after execution
412
self.ipython_widget.executed.connect(self.update_history)
414
#------ Public API --------------------------------------------------------
416
"""Return client name"""
417
return _("Console") + " " + self.client_name
419
def get_control(self):
420
"""Return the text widget (or similar) to give focus to"""
421
# page_control is the widget used for paging
422
page_control = self.ipython_widget._page_control
423
if page_control and page_control.isVisible():
426
return self.ipython_widget._control
428
def get_options_menu(self):
429
"""Return options menu"""
431
self.interrupt_action = create_action(self, _("Interrupt kernel"),
432
icon=get_icon('terminate.png'),
433
triggered=self.interrupt_kernel)
434
self.restart_action = create_action(self, _("Restart kernel"),
435
icon=get_icon('restart.png'),
436
triggered=self.restart_kernel)
439
self.intro_action = create_action(self, _("Intro to IPython"),
440
triggered=self._show_intro)
441
self.quickref_action = create_action(self, _("Quick Reference"),
442
triggered=self._show_quickref)
443
self.guiref_action = create_action(self, _("Console help"),
444
triggered=self._show_guiref)
445
help_menu = QMenu(_("Help"), self)
446
help_action = create_action(self, _("IPython Help"),
447
icon=get_std_icon('DialogHelpButton'))
448
help_action.setMenu(help_menu)
449
add_actions(help_menu, (self.intro_action, self.guiref_action,
450
self.quickref_action))
453
if self.menu_actions is not None:
454
actions = [self.interrupt_action, self.restart_action, None] +\
455
self.menu_actions + [None, help_menu]
457
actions = [self.interrupt_action, self.restart_action, None,
461
def get_toolbar_buttons(self):
462
"""Return toolbar buttons list"""
463
#TODO: Eventually add some buttons (Empty for now)
464
# (see for example: spyderlib/widgets/externalshell/baseshell.py)
466
if self.options_button is None:
467
options = self.get_options_menu()
469
self.options_button = create_toolbutton(self,
470
text=_("Options"), icon=get_icon('tooloptions.png'))
471
self.options_button.setPopupMode(QToolButton.InstantPopup)
473
add_actions(menu, options)
474
self.options_button.setMenu(menu)
475
if self.options_button is not None:
476
buttons.append(self.options_button)
479
def add_actions_to_context_menu(self, menu):
480
"""Add actions to IPython widget context menu"""
481
# See spyderlib/widgets/ipython.py for more details on this method
482
inspect_action = create_action(self, _("Inspect current object"),
483
QKeySequence("Ctrl+I"),
484
icon=get_std_icon('MessageBoxInformation'),
485
triggered=self.inspect_object)
486
clear_line_action = create_action(self, _("Clear line or block"),
487
QKeySequence("Shift+Escape"),
488
icon=get_icon('eraser.png'),
489
triggered=self.clear_line)
490
clear_console_action = create_action(self, _("Clear console"),
491
QKeySequence("Ctrl+L"),
492
icon=get_icon('clear.png'),
493
triggered=self.clear_console)
494
quit_action = create_action(self, _("&Quit"), icon='exit.png',
495
triggered=self.exit_callback)
496
add_actions(menu, (None, inspect_action, clear_line_action,
497
clear_console_action, None, quit_action))
500
def set_font(self, font):
501
"""Set IPython widget's font"""
502
self.ipython_widget.font = font
504
def interrupt_kernel(self):
505
"""Interrupt the associanted Spyder kernel if it's running"""
506
self.ipython_widget.request_interrupt_kernel()
508
def restart_kernel(self):
509
"""Restart the associanted Spyder kernel"""
510
self.ipython_widget.request_restart_kernel()
512
def inspect_object(self):
513
"""Show how to inspect an object with our object inspector"""
514
self.ipython_widget._control.inspect_current_object()
516
def clear_line(self):
517
"""Clear a console line"""
518
self.ipython_widget._keyboard_quit()
520
def clear_console(self):
521
"""Clear the whole console"""
522
self.ipython_widget.execute("%clear")
524
def if_kernel_dies(self, t):
526
Show a message in the console if the kernel dies.
527
t is the time in seconds between the death and showing the message.
529
message = _("It seems the kernel died unexpectedly. Use "
530
"'Restart kernel' to continue using this console.")
531
self.ipython_widget._append_plain_text(message + '\n')
533
def update_history(self):
534
self.history = self.ipython_widget._history
536
def interrupt_message(self):
538
Print an interrupt message when the client is connected to an external
541
message = _("Kernel process is either remote or unspecified. "
543
QMessageBox.information(self, "IPython", message)
545
def restart_message(self):
547
Print a restart message when the client is connected to an external
550
message = _("Kernel process is either remote or unspecified. "
552
QMessageBox.information(self, "IPython", message)
554
#------ Private API -------------------------------------------------------
555
def _show_rich_help(self, text):
556
"""Use our Object Inspector to show IPython help texts in rich mode"""
557
from spyderlib.utils.inspector import sphinxify as spx
559
context = spx.generate_context(title='', argspec='', note='',
561
html_text = spx.sphinxify(text, context)
562
inspector = self.get_control().inspector
563
inspector.switch_to_rich_text()
564
inspector.set_rich_text_html(html_text,
565
QUrl.fromLocalFile(spx.CSS_PATH))
567
def _show_plain_help(self, text):
568
"""Use our Object Inspector to show IPython help texts in plain mode"""
569
inspector = self.get_control().inspector
570
inspector.switch_to_plain_text()
571
inspector.set_plain_text(text, is_code=False)
573
def _show_intro(self):
574
"""Show intro to IPython help"""
575
from IPython.core.usage import interactive_usage
576
self._show_rich_help(interactive_usage)
578
def _show_guiref(self):
579
"""Show qtconsole help"""
580
from IPython.core.usage import gui_reference
581
self._show_rich_help(gui_reference)
583
def _show_quickref(self):
584
"""Show IPython Cheat Sheet"""
585
from IPython.core.usage import quick_reference
586
self._show_plain_help(quick_reference)
588
#---- Qt methods ----------------------------------------------------------
589
def closeEvent(self, event):
590
"""Reimplement Qt method to stop sending the custom_restart_kernel_died
592
self.ipython_widget.custom_restart = False
595
class IPythonConsole(SpyderPluginWidget):
596
"""IPython Console plugin"""
597
CONF_SECTION = 'ipython_console'
598
CONFIGWIDGET_CLASS = IPythonConsoleConfigPage
599
def __init__(self, parent):
600
SpyderPluginWidget.__init__(self, parent)
602
self.ipython_app = None
603
self.initialize_application()
605
self.tabwidget = None
606
self.menu_actions = None
608
self.inspector = None # Object inspector plugin
609
self.historylog = None # History log plugin
611
self.shellwidgets = []
614
self.initialize_plugin()
616
layout = QVBoxLayout()
617
self.tabwidget = Tabs(self, self.menu_actions)
618
if hasattr(self.tabwidget, 'setDocumentMode')\
619
and not sys.platform == 'darwin':
620
# Don't set document mode to true on OSX because it generates
621
# a crash when the console is detached from the main window
623
self.tabwidget.setDocumentMode(True)
624
self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
626
self.connect(self.tabwidget, SIGNAL('move_data(int,int)'),
629
self.tabwidget.set_close_function(self.close_console)
631
layout.addWidget(self.tabwidget)
633
# Find/replace widget
634
self.find_widget = FindReplace(self)
635
self.find_widget.hide()
636
self.register_widget_shortcuts("Editor", self.find_widget)
637
layout.addWidget(self.find_widget)
639
self.setLayout(layout)
642
self.setAcceptDrops(True)
644
#------ SpyderPluginWidget API --------------------------------------------
645
def get_plugin_title(self):
646
"""Return widget title"""
647
return _('IPython console')
649
def get_plugin_icon(self):
650
"""Return widget icon"""
651
return get_icon('ipython_console.png')
653
def get_focus_widget(self):
655
Return the widget to give focus to when
656
this plugin's dockwidget is raised on top-level
658
shellwidget = self.tabwidget.currentWidget()
659
if shellwidget is not None:
660
return shellwidget.get_control()
662
def get_plugin_actions(self):
663
"""Return a list of actions related to plugin"""
664
client_action = create_action(self, _("Connect to an existing kernel"),
666
'ipython_console.png',
667
_("Open a new IPython client connected to an external kernel"),
668
triggered=self.new_client)
670
# Add the action to the 'Interpreters' menu on the main window
671
interact_menu_actions = [None, client_action]
672
self.main.interact_menu_actions += interact_menu_actions
675
console = self.main.extconsole
676
self.menu_actions = [console.ipython_kernel_action, client_action]
678
return self.menu_actions
680
def register_plugin(self):
681
"""Register plugin in Spyder's main window"""
682
self.main.add_dockwidget(self)
683
self.inspector = self.main.inspector
684
self.historylog = self.main.historylog
685
self.connect(self, SIGNAL('focus_changed()'),
686
self.main.plugin_focus_changed)
688
self.connect(self, SIGNAL("edit_goto(QString,int,QString)"),
689
self.main.editor.load)
691
def closing_plugin(self, cancelable=False):
692
"""Perform actions before parent main window is closed"""
693
for shellwidget in self.shellwidgets:
697
def refresh_plugin(self):
698
"""Refresh tabwidget"""
700
if self.tabwidget.count():
701
# Give focus to the control widget of selected tab
702
clientwidget = self.tabwidget.currentWidget()
703
control = clientwidget.get_control()
705
widgets = clientwidget.get_toolbar_buttons()+[5]
707
# Change extconsole tab to the client's kernel widget
708
idx = self.main.extconsole.get_shell_index_from_id(
709
clientwidget.kernel_widget_id)
711
self.main.extconsole.tabwidget.setCurrentIndex(idx)
715
self.find_widget.set_editor(control)
716
self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets})
717
self.emit(SIGNAL('update_plugin_title()'))
719
def apply_plugin_settings(self, options):
720
"""Apply configuration file's plugin settings"""
721
font = self.get_plugin_font()
722
for shellwidget in self.shellwidgets:
723
shellwidget.set_font(font)
725
#------ Public API --------------------------------------------------------
726
def get_clients(self):
727
"""Return IPython client widgets list"""
728
return [sw for sw in self.shellwidgets
729
if isinstance(sw, IPythonClient)]
731
# def get_kernels(self):
732
# """Return IPython kernel widgets list"""
733
# return [sw for sw in self.shellwidgets
734
# if isinstance(sw, IPythonKernel)]
737
def get_focus_client(self):
738
"""Return client shellwidget which has focus, if any"""
739
widget = QApplication.focusWidget()
740
for client in self.get_clients():
741
if widget is client or widget is client.get_control():
744
def new_client(self, connection_file=None, kernel_widget_id=None):
745
"""Create a new IPython client"""
748
example = _('(for example: `kernel-3764.json`, or simply `3764`)')
750
cf, valid = QInputDialog.getText(self, _('IPython'),
751
_('Provide an IPython kernel connection file:')+\
756
match = re.match('(kernel-|^)([a-fA-F0-9-]+)(.json|$)', cf)
757
kernel_num = match.groups()[1]
759
cf = 'kernel-%s.json' % kernel_num
764
# Generating the client name and setting kernel_widget_id
765
match = re.match('^kernel-([a-fA-F0-9-]+).json', cf)
768
client_name = match.groups()[0]+'/'+chr(65+count)
769
for clw in self.get_clients():
770
if clw.client_name == client_name:
771
kernel_widget_id = clw.kernel_widget_id
777
# Trying to get kernel_widget_id from the currently opened kernels if
778
# the previous procedure fails. This could happen when the first
779
# client connected to a kernel is closed but the kernel is left open
780
# and you try to connect new clients to it
781
if kernel_widget_id is None:
782
console = self.main.extconsole
783
for sw in console.shellwidgets:
784
if sw.connection_file == cf:
785
kernel_widget_id = id(sw)
787
# Creating the IPython client widget
789
self.register_client(cf, kernel_widget_id, client_name)
790
except (IOError, UnboundLocalError):
791
QMessageBox.critical(self, _('IPython'),
792
_("Unable to connect to IPython kernel "
796
def client_config(self):
797
"""Generate a Config instance for IPython clients using our config
800
This let us create each client with its own config (as oppossed to
801
IPythonQtConsoleApp, where all clients have the same config)
803
# ---- IPython config ----
805
profile_path = osp.join(get_ipython_dir(), 'profile_default')
806
full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'],
809
# From the full config we only select the IPythonWidget section
810
# because the others have no effect here.
811
ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget})
815
# ---- Spyder config ----
818
# Make the pager widget a rich one (i.e a QTextEdit)
819
spy_cfg.IPythonWidget.kind = 'rich'
821
# Gui completion widget
822
gui_comp_o = self.get_option('use_gui_completion')
823
completions = {True: 'droplist', False: 'ncurses'}
824
spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o]
827
pager_o = self.get_option('use_pager')
829
spy_cfg.IPythonWidget.paging = 'inside'
831
spy_cfg.IPythonWidget.paging = 'none'
834
calltips_o = self.get_option('show_calltips')
835
spy_cfg.IPythonWidget.enable_calltips = calltips_o
838
buffer_size_o = self.get_option('buffer_size')
839
spy_cfg.IPythonWidget.buffer_size = buffer_size_o
842
in_prompt_o = self.get_option('in_prompt')
843
out_prompt_o = self.get_option('out_prompt')
845
spy_cfg.IPythonWidget.in_prompt = in_prompt_o
847
spy_cfg.IPythonWidget.out_prompt = out_prompt_o
849
# Merge IPython and Spyder configs. Spyder prefs will have prevalence
851
ip_cfg._merge(spy_cfg)
854
def initialize_application(self):
855
"""Initialize IPython application"""
856
#======================================================================
857
# For IPython developers review [1]
858
self.ipython_app = IPythonApp()
859
# Is the following line really necessary?
860
#self.ipython_app.initialize_all_except_qt()
861
#======================================================================
863
def register_client(self, connection_file, kernel_widget_id, client_name):
864
"""Register new IPython client"""
865
#======================================================================
866
# For IPython developers review [2]
867
ipython_widget = self.ipython_app.create_new_client(connection_file,
868
config=self.client_config())
869
#======================================================================
871
shellwidget = IPythonClient(self, connection_file, kernel_widget_id,
872
client_name, ipython_widget,
873
history_filename='.history.py',
874
menu_actions=self.menu_actions)
876
control = shellwidget.ipython_widget._control
877
page_control = shellwidget.ipython_widget._page_control
880
self.connect(control, SIGNAL("go_to_error(QString)"), self.go_to_error)
882
# Handle kernel interrupt
883
extconsoles = self.main.extconsole.shellwidgets
886
if extconsoles[-1].connection_file == connection_file:
887
spyder_kernel = extconsoles[-1]
888
ipython_widget.custom_interrupt_requested.connect(
889
spyder_kernel.keyboard_interrupt)
890
if spyder_kernel is None:
891
ipython_widget.custom_interrupt_requested.connect(
892
shellwidget.interrupt_message)
894
# Handle kernel restarts asked by the user
895
if spyder_kernel is not None:
896
ipython_widget.custom_restart_requested.connect(
897
self.create_new_kernel)
899
ipython_widget.custom_restart_requested.connect(
900
shellwidget.restart_message)
902
# Print a message if kernel dies unexpectedly
903
ipython_widget.custom_restart_kernel_died.connect(
904
lambda t: shellwidget.if_kernel_dies(t))
906
# Connect text widget to our inspector
907
if spyder_kernel is not None and self.inspector is not None:
908
control.set_inspector(self.inspector)
910
# Connect client to our history log
911
if self.historylog is not None:
912
self.historylog.add_history(shellwidget.history_filename)
913
self.connect(shellwidget,
914
SIGNAL('append_to_history(QString,QString)'),
915
self.historylog.append_to_history)
917
# Apply settings to newly created client widget:
918
shellwidget.set_font( self.get_plugin_font() )
920
# Add tab and change focus to it
921
self.add_tab(shellwidget, name=shellwidget.get_name())
922
self.connect(shellwidget, SIGNAL('focus_changed()'),
923
lambda: self.emit(SIGNAL('focus_changed()')))
925
# Update the find widget if focus changes between control and
927
self.find_widget.set_editor(control)
929
self.connect(control, SIGNAL('visibility_changed(bool)'),
931
self.connect(page_control, SIGNAL('visibility_changed(bool)'),
933
self.connect(page_control, SIGNAL('show_find_widget()'),
934
self.find_widget.show)
936
def close_related_ipython_clients(self, client):
937
"""Close all IPython clients related to *client*, except itself"""
938
for clw in self.shellwidgets[:]:
939
if clw is not client and\
940
clw.connection_file == client.connection_file:
941
self.close_console(widget=clw)
943
def get_ipython_widget(self, kernel_widget_id):
944
"""Return IPython widget (ipython_plugin.ipython_widget)
945
associated to kernel_widget_id"""
946
for clw in self.shellwidgets:
947
if clw.kernel_widget_id == kernel_widget_id:
948
return clw.ipython_widget
950
raise ValueError, "Unknown kernel widget ID %r" % kernel_widget_id
952
def add_tab(self, widget, name):
954
self.shellwidgets.append(widget)
955
index = self.tabwidget.addTab(widget, get_icon('ipython_console.png'),
957
self.tabwidget.setCurrentIndex(index)
958
if self.dockwidget and not self.ismaximized:
959
self.dockwidget.setVisible(True)
960
self.dockwidget.raise_()
961
widget.get_control().setFocus()
963
def move_tab(self, index_from, index_to):
965
Move tab (tabs themselves have already been moved by the tabwidget)
967
shell = self.shellwidgets.pop(index_from)
968
self.shellwidgets.insert(index_to, shell)
969
self.emit(SIGNAL('update_plugin_title()'))
971
def close_console(self, index=None, widget=None, force=False):
972
"""Close console tab from index or widget (or close current tab)"""
973
if not self.tabwidget.count():
975
if widget is not None:
976
index = self.tabwidget.indexOf(widget)
977
if index is None and widget is None:
978
index = self.tabwidget.currentIndex()
979
if index is not None:
980
widget = self.tabwidget.widget(index)
982
# Check if related clients or kernels are opened
983
# and eventually ask before closing them
984
if not force and isinstance(widget, IPythonClient):
985
console = self.main.extconsole
986
idx = console.get_shell_index_from_id(widget.kernel_widget_id)
989
if self.get_option('ask_before_closing'):
990
ans = QMessageBox.question(self, self.get_plugin_title(),
991
_("%s will be closed.\n"
992
"Do you want to kill the associated kernel "
993
"and all of its clients?") % widget.get_name(),
994
QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
995
if ans == QMessageBox.Cancel:
997
close_all = ans == QMessageBox.Yes
999
console.close_console(index=idx, from_ipython_client=True)
1000
self.close_related_ipython_clients(widget)
1003
# Note: widget index may have changed after closing related widgets
1004
self.tabwidget.removeTab(self.tabwidget.indexOf(widget))
1005
self.shellwidgets.remove(widget)
1007
self.emit(SIGNAL('update_plugin_title()'))
1009
def go_to_error(self, text):
1010
"""Go to error if relevant"""
1011
match = get_error_match(unicode(text))
1013
fname, lnb = match.groups()
1014
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
1015
osp.abspath(fname), int(lnb), '')
1017
def get_shell_index_from_id(self, shell_id):
1018
"""Return shellwidget index from id"""
1019
for index, shell in enumerate(self.shellwidgets):
1020
if id(shell) == shell_id:
1023
def rename_ipython_client_tab(self, connection_file, client_widget_id):
1024
"""Add the pid of the kernel process to an IPython client tab"""
1025
index = self.get_shell_index_from_id(client_widget_id)
1026
match = re.match('^kernel-(\d+).json', connection_file)
1027
if match is not None: # should not fail, but we never know...
1028
name = _("Console") + " " + match.groups()[0] + '/' + chr(65)
1029
self.tabwidget.setTabText(index, name)
1031
def create_new_kernel(self):
1032
"""Create a new kernel if the user asks for it"""
1033
# Took this bit of code (until if result == ) from the IPython project
1034
# (frontend/qt/frontend_widget.py - restart_kernel).
1035
# Licensed under the BSD license
1036
message = _('Are you sure you want to restart the kernel?')
1037
buttons = QMessageBox.Yes | QMessageBox.No
1038
result = QMessageBox.question(self, _('Restart kernel?'),
1040
if result == QMessageBox.Yes:
1041
console = self.main.extconsole
1042
console.start_ipython_kernel(create_client=False)
1043
kernel_widget = console.shellwidgets[-1]
1044
self.connect(kernel_widget,
1045
SIGNAL('create_ipython_client(QString)'),
1046
lambda cf: self.connect_to_new_kernel(cf, kernel_widget))
1048
def connect_to_new_kernel(self, connection_file, kernel_widget):
1050
After a new kernel is created, execute this action to connect the new
1051
kernel to the old client
1053
console = self.main.extconsole
1054
shellwidget = self.tabwidget.currentWidget()
1056
# Close old kernel tab
1057
idx = console.get_shell_index_from_id(shellwidget.kernel_widget_id)
1058
console.close_console(index=idx, from_ipython_client=True)
1060
# Set attributes for the new kernel
1061
console.set_ipython_kernel_attrs(connection_file, kernel_widget)
1063
# Connect client to new kernel
1064
kernel_manager = self.ipython_app.create_kernel_manager(connection_file)
1065
shellwidget.ipython_widget.kernel_manager = kernel_manager
1066
shellwidget.kernel_widget_id = id(kernel_widget)
1067
shellwidget.get_control().setFocus()
1070
client_widget_id = id(shellwidget)
1071
self.rename_ipython_client_tab(connection_file, client_widget_id)
1074
#TODO: try and reimplement this block
1075
# (this is still the original code block copied from externalconsole.py)
1076
# def dragEnterEvent(self, event):
1077
# """Reimplement Qt method
1078
# Inform Qt about the types of data that the widget accepts"""
1079
# source = event.mimeData()
1080
# if source.hasUrls():
1081
# if mimedata2url(source):
1082
# pathlist = mimedata2url(source)
1083
# shellwidget = self.tabwidget.currentWidget()
1084
# if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1085
# event.acceptProposedAction()
1086
# elif shellwidget is None or not shellwidget.is_running():
1089
# event.acceptProposedAction()
1092
# elif source.hasText():
1093
# event.acceptProposedAction()
1095
# def dropEvent(self, event):
1096
# """Reimplement Qt method
1097
# Unpack dropped data and handle it"""
1098
# source = event.mimeData()
1099
# shellwidget = self.tabwidget.currentWidget()
1100
# if source.hasText():
1101
# qstr = source.text()
1102
# if is_python_script(unicode(qstr)):
1105
# shellwidget.shell.insert_text(qstr)
1106
# elif source.hasUrls():
1107
# pathlist = mimedata2url(source)
1108
# if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1109
# for fname in pathlist:
1112
# shellwidget.shell.drop_pathlist(pathlist)
1113
# event.acceptProposedAction()
1
# -*- coding: utf-8 -*-
3
# Copyright © 2012 Pierre Raybaut
4
# Licensed under the terms of the MIT License
5
# (see spyderlib/__init__.py for details)
7
"""IPython Console plugin
9
Handles IPython clients (and in the future, will handle IPython kernels too
10
-- meanwhile, the external console plugin is handling them)"""
12
# pylint: disable=C0103
13
# pylint: disable=R0903
14
# pylint: disable=R0911
15
# pylint: disable=R0201
17
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QWidget, QGroupBox,
18
QLineEdit, QInputDialog, QTabWidget, QMenu,
19
QFontComboBox, QHBoxLayout, QApplication,
20
QToolButton, QLabel, QKeySequence)
21
from spyderlib.qt.QtCore import SIGNAL, Qt, QUrl
29
from IPython.config.loader import Config, load_pyconfig_files
30
from IPython.core.application import get_ipython_dir
33
from spyderlib.baseconfig import get_conf_path, _
34
from spyderlib.utils import programs
35
from spyderlib.utils.misc import (get_error_match,
36
remove_trailing_single_backslash)
37
from spyderlib.utils.qthelpers import (get_icon, get_std_icon, create_action,
38
create_toolbutton, add_actions)
39
from spyderlib.widgets.tabs import Tabs
40
from spyderlib.widgets.ipython import IPythonApp
41
from spyderlib.widgets.findreplace import FindReplace
42
from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage
43
from spyderlib.widgets.sourcecode import mixins
46
class IPythonConsoleConfigPage(PluginConfigPage):
47
def __init__(self, plugin, parent):
48
PluginConfigPage.__init__(self, plugin, parent)
49
self.get_name = lambda: _("IPython console")
52
newcb = self.create_checkbox
53
mpl_present = programs.is_module_installed("matplotlib")
56
font_group = self.create_fontgroup(option=None, text=None,
57
fontfilters=QFontComboBox.MonospacedFonts)
60
interface_group = QGroupBox(_("Interface"))
61
banner_box = newcb(_("Display initial banner"), 'show_banner',
62
tip=_("This option lets you hide the message shown at\n"
63
"the top of the console when it's opened."))
64
gui_comp_box = newcb(_("Use a completion widget"),
66
tip=_("Use a widget instead of plain text "
67
"output for tab completion"))
68
pager_box = newcb(_("Use a pager to display additional text inside "
69
"the console"), 'use_pager',
70
tip=_("Useful if you don't want to fill the "
71
"console with long help or completion texts.\n"
72
"Note: Use the Q key to get out of the "
74
calltips_box = newcb(_("Display balloon tips"), 'show_calltips')
75
ask_box = newcb(_("Ask for confirmation before closing"),
78
interface_layout = QVBoxLayout()
79
interface_layout.addWidget(banner_box)
80
interface_layout.addWidget(gui_comp_box)
81
interface_layout.addWidget(pager_box)
82
interface_layout.addWidget(calltips_box)
83
interface_layout.addWidget(ask_box)
84
interface_group.setLayout(interface_layout)
86
# Background Color Group
87
bg_group = QGroupBox(_("Background color"))
88
light_radio = self.create_radiobutton(_("Light background"),
90
dark_radio = self.create_radiobutton(_("Dark background"),
92
bg_layout = QVBoxLayout()
93
bg_layout.addWidget(light_radio)
94
bg_layout.addWidget(dark_radio)
95
bg_group.setLayout(bg_layout)
98
source_code_group = QGroupBox(_("Source code"))
99
buffer_spin = self.create_spinbox(
100
_("Buffer: "), _(" lines"),
101
'buffer_size', min_=-1, max_=1000000, step=100,
102
tip=_("Set the maximum number of lines of text shown in the\n"
103
"console before truncation. Specifying -1 disables it\n"
104
"(not recommended!)"))
105
source_code_layout = QVBoxLayout()
106
source_code_layout.addWidget(buffer_spin)
107
source_code_group.setLayout(source_code_layout)
111
pylab_group = QGroupBox(_("Support for graphics (Pylab)"))
112
pylab_box = newcb(_("Activate support"), 'pylab')
113
autoload_pylab_box = newcb(_("Automatically load Pylab and NumPy"),
115
tip=_("This lets you load graphics support "
116
"without importing \nthe commands to do "
117
"plots. Useful to work with other\n"
118
"plotting libraries different to "
119
"Matplotlib or to develop \nGUIs with "
121
autoload_pylab_box.setEnabled(self.get_option('pylab') and mpl_present)
122
self.connect(pylab_box, SIGNAL("toggled(bool)"),
123
autoload_pylab_box.setEnabled)
125
pylab_layout = QVBoxLayout()
126
pylab_layout.addWidget(pylab_box)
127
pylab_layout.addWidget(autoload_pylab_box)
128
pylab_group.setLayout(pylab_layout)
131
self.set_option('pylab', False)
132
self.set_option('pylab/autoload', False)
133
pylab_group.setEnabled(False)
134
pylab_tip = _("This feature requires the Matplotlib library.\n"
135
"It seems you don't have it installed.")
136
pylab_box.setToolTip(pylab_tip)
138
# Pylab backend Group
140
automatic = _("Automatic")
141
backend_group = QGroupBox(_("Graphics backend"))
142
bend_label = QLabel(_("Decide how graphics are going to be displayed "
143
"in the console. If unsure, please select "
144
"<b>%s</b> to put graphics inside the "
145
"console or <b>%s</b> to interact with "
146
"them (through zooming and panning) in a "
147
"separate window.") % (inline, automatic))
148
bend_label.setWordWrap(True)
150
backends = [(inline, 0), (automatic, 1), ("Qt", 2)]
151
# TODO: Add gtk3 when 0.13 is released
152
if sys.platform == 'darwin':
153
backends.append( ("Mac OSX", 3) )
154
if programs.is_module_installed('pygtk'):
155
backends.append( ("Gtk", 4) )
156
if programs.is_module_installed('wxPython'):
157
backends.append( ("Wx", 5) )
158
if programs.is_module_installed('_tkinter'):
159
backends.append( ("Tkinter", 6) )
160
backends = tuple(backends)
162
backend_box = self.create_combobox( _("Backend:")+" ", backends,
163
'pylab/backend', default=0,
164
tip=_("This option will be applied the "
165
"next time a console is opened."))
167
backend_layout = QVBoxLayout()
168
backend_layout.addWidget(bend_label)
169
backend_layout.addWidget(backend_box)
170
backend_group.setLayout(backend_layout)
171
backend_group.setEnabled(self.get_option('pylab') and mpl_present)
172
self.connect(pylab_box, SIGNAL("toggled(bool)"),
173
backend_group.setEnabled)
175
# Inline backend Group
176
inline_group = QGroupBox(_("Inline backend"))
177
inline_label = QLabel(_("Decide how to render the figures created by "
179
inline_label.setWordWrap(True)
180
formats = (("PNG", 0), ("SVG", 1))
181
format_box = self.create_combobox(_("Format:")+" ", formats,
182
'pylab/inline/figure_format', default=0)
183
resolution_spin = self.create_spinbox(
184
_("Resolution:")+" ", " "+_("dpi"),
185
'pylab/inline/resolution', min_=56, max_=112, step=1,
186
tip=_("Only used when the format is PNG. Default is "
188
width_spin = self.create_spinbox(
189
_("Width:")+" ", " "+_("inches"),
190
'pylab/inline/width', min_=4, max_=20, step=1,
191
tip=_("Default is 6"))
192
height_spin = self.create_spinbox(
193
_("Height:")+" ", " "+_("inches"),
194
'pylab/inline/height', min_=4, max_=20, step=1,
195
tip=_("Default is 4"))
197
inline_layout = QVBoxLayout()
198
inline_layout.addWidget(inline_label)
199
inline_layout.addWidget(format_box)
200
inline_layout.addWidget(resolution_spin)
201
inline_layout.addWidget(width_spin)
202
inline_layout.addWidget(height_spin)
203
inline_group.setLayout(inline_layout)
204
inline_group.setEnabled(self.get_option('pylab') and mpl_present)
205
self.connect(pylab_box, SIGNAL("toggled(bool)"),
206
inline_group.setEnabled)
210
run_lines_group = QGroupBox(_("Run code"))
211
run_lines_label = QLabel(_("You can run several lines of code when "
212
"a console is started. Please introduce "
213
"each one separated by commas, for "
215
"<i>import os, import sys</i>"))
216
run_lines_label.setWordWrap(True)
217
run_lines_edit = self.create_lineedit(_("Lines:"), 'startup/run_lines',
218
'', alignment=Qt.Horizontal)
220
run_lines_layout = QVBoxLayout()
221
run_lines_layout.addWidget(run_lines_label)
222
run_lines_layout.addWidget(run_lines_edit)
223
run_lines_group.setLayout(run_lines_layout)
226
run_file_group = QGroupBox(_("Run a file"))
227
run_file_label = QLabel(_("You can also run a whole file at startup "
228
"instead of just some lines (This is "
229
"similar to have a PYTHONSTARTUP file)."))
230
run_file_label.setWordWrap(True)
231
file_radio = newcb(_("Use the following file:"),
232
'startup/use_run_file', False)
233
run_file_browser = self.create_browsefile('', 'startup/run_file', '')
234
run_file_browser.setEnabled(False)
235
self.connect(file_radio, SIGNAL("toggled(bool)"),
236
run_file_browser.setEnabled)
238
run_file_layout = QVBoxLayout()
239
run_file_layout.addWidget(run_file_label)
240
run_file_layout.addWidget(file_radio)
241
run_file_layout.addWidget(run_file_browser)
242
run_file_group.setLayout(run_file_layout)
245
spyder_group = QGroupBox(_("Spyder startup"))
246
ipystartup_box = newcb(_("Open an IPython console at startup"),
247
"open_ipython_at_startup")
248
spyder_layout = QVBoxLayout()
249
spyder_layout.addWidget(ipystartup_box)
250
spyder_group.setLayout(spyder_layout)
252
# ---- Advanced settings ----
253
# Greedy completer group
254
greedy_group = QGroupBox(_("Greedy completion"))
255
greedy_label = QLabel(_("Enable <tt>Tab</tt> completion on elements "
256
"of lists, results of function calls, etc, "
257
"<i>without</i> assigning them to a "
259
"For example, you can get completions on "
260
"things like <tt>li[0].<Tab></tt> or "
261
"<tt>ins.meth().<Tab></tt>"))
262
greedy_label.setWordWrap(True)
263
greedy_box = newcb(_("Use the greedy completer"), "greedy_completer",
264
tip="<b>Warning</b>: It can be unsafe because the "
265
"code is actually evaluated when you press "
268
greedy_layout = QVBoxLayout()
269
greedy_layout.addWidget(greedy_label)
270
greedy_layout.addWidget(greedy_box)
271
greedy_group.setLayout(greedy_layout)
274
autocall_group = QGroupBox(_("Autocall"))
275
autocall_label = QLabel(_("Autocall makes IPython automatically call "
276
"any callable object even if you didn't type "
277
"explicit parentheses.<br>"
278
"For example, if you type <i>str 43</i> it "
279
"becomes <i>str(43)</i> automatically."))
280
autocall_label.setWordWrap(True)
284
autocall_opts = ((_('Off'), 0), (smart, 1), (full, 2))
285
autocall_box = self.create_combobox(
286
_("Autocall: "), autocall_opts, 'autocall', default=0,
287
tip=_("On <b>%s</b> mode, Autocall is not applied if "
288
"there are no arguments after the callable. On "
289
"<b>%s</b> mode, all callable objects are "
290
"automatically called (even if no arguments are "
291
"present).") % (smart, full))
293
autocall_layout = QVBoxLayout()
294
autocall_layout.addWidget(autocall_label)
295
autocall_layout.addWidget(autocall_box)
296
autocall_group.setLayout(autocall_layout)
299
sympy_group = QGroupBox(_("Symbolic Mathematics"))
300
sympy_label = QLabel(_("Perfom symbolic operations in the console "
301
"(e.g. integrals, derivatives, vector calculus, "
302
"etc) and get the outputs in a beautifully "
304
sympy_label.setWordWrap(True)
305
sympy_box = newcb(_("Use symbolic math"), "symbolic_math",
306
tip=_("This option loads the Sympy library to work "
307
"with.<br>Please refer to its documentation to "
308
"learn how to use it."))
310
sympy_layout = QVBoxLayout()
311
sympy_layout.addWidget(sympy_label)
312
sympy_layout.addWidget(sympy_box)
313
sympy_group.setLayout(sympy_layout)
315
sympy_present = programs.is_module_installed("sympy")
316
if not sympy_present:
317
self.set_option("symbolic_math", False)
318
sympy_box.setEnabled(False)
319
sympy_tip = _("This feature requires the Sympy library.\n"
320
"It seems you don't have it installed.")
321
sympy_box.setToolTip(sympy_tip)
324
prompts_group = QGroupBox(_("Prompts"))
325
prompts_label = QLabel(_("Modify how Input and Output prompts are "
326
"shown in the console."))
327
prompts_label.setWordWrap(True)
328
in_prompt_edit = self.create_lineedit(_("Input prompt:"),
331
'In [<span class="in-prompt-number">'
332
'%i</span>]:'),
333
alignment=Qt.Horizontal)
334
out_prompt_edit = self.create_lineedit(_("Output prompt:"),
337
'Out[<span class="out-prompt-number">'
338
'%i</span>]:'),
339
alignment=Qt.Horizontal)
341
prompts_layout = QVBoxLayout()
342
prompts_layout.addWidget(prompts_label)
343
prompts_layout.addWidget(in_prompt_edit)
344
prompts_layout.addWidget(out_prompt_edit)
345
prompts_group.setLayout(prompts_layout)
347
# --- Tabs organization ---
349
tabs.addTab(self.create_tab(font_group, interface_group, bg_group,
350
source_code_group), _("Display"))
351
tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group),
353
tabs.addTab(self.create_tab(spyder_group, run_lines_group,
354
run_file_group), _("Startup"))
355
tabs.addTab(self.create_tab(greedy_group, autocall_group, sympy_group,
356
prompts_group), _("Advanced Settings"))
358
vlayout = QVBoxLayout()
359
vlayout.addWidget(tabs)
360
self.setLayout(vlayout)
363
class IPythonClient(QWidget, mixins.SaveHistoryMixin):
365
Spyder IPython client or frontend.
367
This is a layer on top of the IPython Qt widget (i.e. RichIPythonWidget +
368
our additions = SpyderIPythonWidget), which becomes the ipywidget attribute
369
of this class. We are doing this for several reasons:
371
1. To add more variables and methods needed to connect the widget to other
372
Spyder plugins and also increase its funcionality.
373
2. To make it clear what has been added by us to IPython widgets.
374
3. To avoid possible name conflicts between our widgets and theirs (e.g.
375
self.history and self._history, respectively)
378
CONF_SECTION = 'ipython'
379
SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime())
381
def __init__(self, plugin, connection_file, kernel_widget_id, client_name,
382
ipywidget, history_filename, menu_actions=None):
383
super(IPythonClient, self).__init__(plugin)
384
mixins.SaveHistoryMixin.__init__(self)
385
self.options_button = None
387
self.connection_file = connection_file
388
self.kernel_widget_id = kernel_widget_id
389
self.client_name = client_name
390
self.ipywidget = ipywidget
391
self.menu_actions = menu_actions
392
self.history_filename = get_conf_path(history_filename)
395
vlayout = QVBoxLayout()
396
toolbar_buttons = self.get_toolbar_buttons()
397
hlayout = QHBoxLayout()
398
for button in toolbar_buttons:
399
hlayout.addWidget(button)
400
vlayout.addLayout(hlayout)
401
vlayout.setContentsMargins(0, 0, 0, 0)
402
vlayout.addWidget(self.ipywidget)
403
self.setLayout(vlayout)
405
self.exit_callback = lambda: plugin.close_console(client=self)
407
# Connect the IPython widget to this IPython client:
408
# (see spyderlib/widgets/ipython.py for more details about this)
409
ipywidget.set_ipyclient(self)
412
self.ipywidget.executing.connect(
413
lambda c: self.add_to_history(command=c))
415
# To update history after execution
416
self.ipywidget.executed.connect(self.update_history)
418
#------ Public API --------------------------------------------------------
420
"""Return client name"""
421
return _("Console") + " " + self.client_name
423
def get_control(self):
424
"""Return the text widget (or similar) to give focus to"""
425
# page_control is the widget used for paging
426
page_control = self.ipywidget._page_control
427
if page_control and page_control.isVisible():
430
return self.ipywidget._control
432
def get_options_menu(self):
433
"""Return options menu"""
435
self.interrupt_action = create_action(self, _("Interrupt kernel"),
436
icon=get_icon('terminate.png'),
437
triggered=self.interrupt_kernel)
438
self.restart_action = create_action(self, _("Restart kernel"),
439
icon=get_icon('restart.png'),
440
triggered=self.restart_kernel)
443
self.intro_action = create_action(self, _("Intro to IPython"),
444
triggered=self._show_intro)
445
self.quickref_action = create_action(self, _("Quick Reference"),
446
triggered=self._show_quickref)
447
self.guiref_action = create_action(self, _("Console help"),
448
triggered=self._show_guiref)
449
help_menu = QMenu(_("Help"), self)
450
help_action = create_action(self, _("IPython Help"),
451
icon=get_std_icon('DialogHelpButton'))
452
help_action.setMenu(help_menu)
453
add_actions(help_menu, (self.intro_action, self.guiref_action,
454
self.quickref_action))
457
if self.menu_actions is not None:
458
actions = [self.interrupt_action, self.restart_action, None] +\
459
self.menu_actions + [None, help_menu]
461
actions = [self.interrupt_action, self.restart_action, None,
465
def get_toolbar_buttons(self):
466
"""Return toolbar buttons list"""
467
#TODO: Eventually add some buttons (Empty for now)
468
# (see for example: spyderlib/widgets/externalshell/baseshell.py)
470
if self.options_button is None:
471
options = self.get_options_menu()
473
self.options_button = create_toolbutton(self,
474
text=_("Options"), icon=get_icon('tooloptions.png'))
475
self.options_button.setPopupMode(QToolButton.InstantPopup)
477
add_actions(menu, options)
478
self.options_button.setMenu(menu)
479
if self.options_button is not None:
480
buttons.append(self.options_button)
483
def add_actions_to_context_menu(self, menu):
484
"""Add actions to IPython widget context menu"""
485
# See spyderlib/widgets/ipython.py for more details on this method
486
inspect_action = create_action(self, _("Inspect current object"),
487
QKeySequence("Ctrl+I"),
488
icon=get_std_icon('MessageBoxInformation'),
489
triggered=self.inspect_object)
490
clear_line_action = create_action(self, _("Clear line or block"),
491
QKeySequence("Shift+Escape"),
492
icon=get_icon('eraser.png'),
493
triggered=self.clear_line)
494
clear_console_action = create_action(self, _("Clear console"),
495
QKeySequence("Ctrl+L"),
496
icon=get_icon('clear.png'),
497
triggered=self.clear_console)
498
quit_action = create_action(self, _("&Quit"), icon='exit.png',
499
triggered=self.exit_callback)
500
add_actions(menu, (None, inspect_action, clear_line_action,
501
clear_console_action, None, quit_action))
504
def set_font(self, font):
505
"""Set IPython widget's font"""
506
self.ipywidget.font = font
508
def interrupt_kernel(self):
509
"""Interrupt the associanted Spyder kernel if it's running"""
510
self.ipywidget.request_interrupt_kernel()
512
def restart_kernel(self):
513
"""Restart the associanted Spyder kernel"""
514
self.ipywidget.request_restart_kernel()
516
def inspect_object(self):
517
"""Show how to inspect an object with our object inspector"""
518
self.ipywidget._control.inspect_current_object()
520
def clear_line(self):
521
"""Clear a console line"""
522
self.ipywidget._keyboard_quit()
524
def clear_console(self):
525
"""Clear the whole console"""
526
self.ipywidget.execute("%clear")
528
def if_kernel_dies(self, t):
530
Show a message in the console if the kernel dies.
531
t is the time in seconds between the death and showing the message.
533
message = _("It seems the kernel died unexpectedly. Use "
534
"'Restart kernel' to continue using this console.")
535
self.ipywidget._append_plain_text(message + '\n')
537
def update_history(self):
538
self.history = self.ipywidget._history
540
def interrupt_message(self):
542
Print an interrupt message when the client is connected to an external
545
message = _("Kernel process is either remote or unspecified. "
547
QMessageBox.information(self, "IPython", message)
549
def restart_message(self):
551
Print a restart message when the client is connected to an external
554
message = _("Kernel process is either remote or unspecified. "
556
QMessageBox.information(self, "IPython", message)
558
#------ Private API -------------------------------------------------------
559
def _show_rich_help(self, text):
560
"""Use our Object Inspector to show IPython help texts in rich mode"""
561
from spyderlib.utils.inspector import sphinxify as spx
563
context = spx.generate_context(title='', argspec='', note='',
565
html_text = spx.sphinxify(text, context)
566
inspector = self.get_control().inspector
567
inspector.switch_to_rich_text()
568
inspector.set_rich_text_html(html_text,
569
QUrl.fromLocalFile(spx.CSS_PATH))
571
def _show_plain_help(self, text):
572
"""Use our Object Inspector to show IPython help texts in plain mode"""
573
inspector = self.get_control().inspector
574
inspector.switch_to_plain_text()
575
inspector.set_plain_text(text, is_code=False)
577
def _show_intro(self):
578
"""Show intro to IPython help"""
579
from IPython.core.usage import interactive_usage
580
self._show_rich_help(interactive_usage)
582
def _show_guiref(self):
583
"""Show qtconsole help"""
584
from IPython.core.usage import gui_reference
585
self._show_rich_help(gui_reference)
587
def _show_quickref(self):
588
"""Show IPython Cheat Sheet"""
589
from IPython.core.usage import quick_reference
590
self._show_plain_help(quick_reference)
592
#---- Qt methods ----------------------------------------------------------
593
def closeEvent(self, event):
594
"""Reimplement Qt method to stop sending the custom_restart_kernel_died
596
self.ipywidget.custom_restart = False
599
class IPythonConsole(SpyderPluginWidget):
601
IPython Console plugin
603
This is a widget with tabs where each one is an IPythonClient
606
CONF_SECTION = 'ipython_console'
607
CONFIGWIDGET_CLASS = IPythonConsoleConfigPage
608
def __init__(self, parent):
609
SpyderPluginWidget.__init__(self, parent)
611
self.ipython_app = None
612
self.initialize_application()
614
self.tabwidget = None
615
self.menu_actions = None
617
self.inspector = None # Object inspector plugin
618
self.historylog = None # History log plugin
623
self.initialize_plugin()
625
layout = QVBoxLayout()
626
self.tabwidget = Tabs(self, self.menu_actions)
627
if hasattr(self.tabwidget, 'setDocumentMode')\
628
and not sys.platform == 'darwin':
629
# Don't set document mode to true on OSX because it generates
630
# a crash when the console is detached from the main window
632
self.tabwidget.setDocumentMode(True)
633
self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
635
self.connect(self.tabwidget, SIGNAL('move_data(int,int)'),
638
self.tabwidget.set_close_function(self.close_console)
640
layout.addWidget(self.tabwidget)
642
# Find/replace widget
643
self.find_widget = FindReplace(self)
644
self.find_widget.hide()
645
self.register_widget_shortcuts("Editor", self.find_widget)
646
layout.addWidget(self.find_widget)
648
self.setLayout(layout)
651
self.setAcceptDrops(True)
653
#------ SpyderPluginWidget API --------------------------------------------
654
def get_plugin_title(self):
655
"""Return widget title"""
656
return _('IPython console')
658
def get_plugin_icon(self):
659
"""Return widget icon"""
660
return get_icon('ipython_console.png')
662
def get_focus_widget(self):
664
Return the widget to give focus to when
665
this plugin's dockwidget is raised on top-level
667
client = self.tabwidget.currentWidget()
668
if client is not None:
669
return client.get_control()
671
def get_current_client(self):
673
Return the currently selected client
675
client = self.tabwidget.currentWidget()
676
if client is not None:
679
def run_script_in_current_client(self, filename, wdir, args, debug):
680
"""Run script in current client, if any"""
681
norm = lambda text: remove_trailing_single_backslash(unicode(text))
682
client = self.get_current_client()
683
if client is not None:
684
# Internal kernels, use runfile
685
if client.kernel_widget_id is not None:
686
line = "%s(r'%s'" % ('debugfile' if debug else 'runfile',
689
line += ", args=r'%s'" % norm(args)
691
line += ", wdir=r'%s'" % norm(wdir)
693
else: # External kernels, use %run
697
line += "\"%s\"" % unicode(filename)
699
line += " %s" % norm(args)
700
self.execute_python_code(line)
702
def execute_python_code(self, lines):
703
client = self.get_current_client()
704
if client is not None:
705
client.ipywidget.execute(unicode(lines))
706
self.activateWindow()
707
client.get_control().setFocus()
709
def write_to_stdin(self, line):
710
client = self.get_current_client()
711
if client is not None:
712
client.ipywidget.write_to_stdin(line)
714
def get_plugin_actions(self):
715
"""Return a list of actions related to plugin"""
716
client_action = create_action(self, _("Connect to an existing kernel"),
718
'ipython_console.png',
719
_("Open a new IPython client connected to an external kernel"),
720
triggered=self.new_client)
722
# Add the action to the 'Interpreters' menu on the main window
723
interact_menu_actions = [None, client_action]
724
self.main.interact_menu_actions += interact_menu_actions
727
extconsole = self.main.extconsole
728
self.menu_actions = [extconsole.ipykernel_action, client_action]
730
return self.menu_actions
732
def register_plugin(self):
733
"""Register plugin in Spyder's main window"""
734
self.main.add_dockwidget(self)
735
self.inspector = self.main.inspector
736
self.historylog = self.main.historylog
737
self.connect(self, SIGNAL('focus_changed()'),
738
self.main.plugin_focus_changed)
739
self.connect(self, SIGNAL("edit_goto(QString,int,QString)"),
740
self.main.editor.load)
741
self.connect(self.main.editor,
742
SIGNAL('run_in_current_ipyclient(QString,QString,QString,bool)'),
743
self.run_script_in_current_client)
745
def closing_plugin(self, cancelable=False):
746
"""Perform actions before parent main window is closed"""
747
for client in self.clients:
751
def refresh_plugin(self):
752
"""Refresh tabwidget"""
754
if self.tabwidget.count():
755
# Give focus to the control widget of the selected tab
756
client = self.tabwidget.currentWidget()
757
control = client.get_control()
759
widgets = client.get_toolbar_buttons()+[5]
761
# Change extconsole tab to the client's kernel widget
762
idx = self.main.extconsole.get_shell_index_from_id(
763
client.kernel_widget_id)
765
self.main.extconsole.tabwidget.setCurrentIndex(idx)
769
self.find_widget.set_editor(control)
770
self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets})
771
self.emit(SIGNAL('update_plugin_title()'))
773
def apply_plugin_settings(self, options):
774
"""Apply configuration file's plugin settings"""
775
font = self.get_plugin_font()
776
for client in self.clients:
777
client.set_font(font)
779
#------ Public API --------------------------------------------------------
780
def get_clients(self):
781
"""Return IPython client widgets list"""
782
return [cl for cl in self.clients if isinstance(cl, IPythonClient)]
784
# def get_kernels(self):
785
# """Return IPython kernel widgets list"""
786
# return [sw for sw in self.shellwidgets
787
# if isinstance(sw, IPythonKernel)]
790
def get_focus_client(self):
791
"""Return current client with focus, if any"""
792
widget = QApplication.focusWidget()
793
for client in self.get_clients():
794
if widget is client or widget is client.get_control():
797
def new_client(self, connection_file=None, kernel_widget_id=None):
798
"""Create a new IPython client"""
801
example = _('(for example: `kernel-3764.json`, or simply `3764`)')
803
cf, valid = QInputDialog.getText(self, _('IPython'),
804
_('Provide an IPython kernel connection file:')+\
809
match = re.match('(kernel-|^)([a-fA-F0-9-]+)(.json|$)', cf)
810
kernel_num = match.groups()[1]
812
cf = 'kernel-%s.json' % kernel_num
817
# Generating the client name and setting kernel_widget_id
818
match = re.match('^kernel-([a-fA-F0-9-]+).json', cf)
821
client_name = match.groups()[0]+'/'+chr(65+count)
822
for cl in self.get_clients():
823
if cl.client_name == client_name:
824
kernel_widget_id = cl.kernel_widget_id
830
# Trying to get kernel_widget_id from the currently opened kernels if
831
# the previous procedure fails. This could happen when the first
832
# client connected to a kernel is closed but the kernel is left open
833
# and you try to connect new clients to it
834
if kernel_widget_id is None:
835
extconsole = self.main.extconsole
836
for sw in extconsole.shellwidgets:
837
if sw.connection_file == cf:
838
kernel_widget_id = id(sw)
840
# Creating the IPython client widget
842
self.register_client(cf, kernel_widget_id, client_name)
843
except (IOError, UnboundLocalError):
844
QMessageBox.critical(self, _('IPython'),
845
_("Unable to connect to IPython kernel "
849
def ipywidget_config(self):
850
"""Generate a Config instance for IPython widgets using our config
853
This let us create each widget with its own config (as oppossed to
854
IPythonQtConsoleApp, where all widgets have the same config)
856
# ---- IPython config ----
858
profile_path = osp.join(get_ipython_dir(), 'profile_default')
859
full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'],
862
# From the full config we only select the IPythonWidget section
863
# because the others have no effect here.
864
ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget})
868
# ---- Spyder config ----
871
# Make the pager widget a rich one (i.e a QTextEdit)
872
spy_cfg.IPythonWidget.kind = 'rich'
874
# Gui completion widget
875
gui_comp_o = self.get_option('use_gui_completion')
876
completions = {True: 'droplist', False: 'ncurses'}
877
spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o]
880
pager_o = self.get_option('use_pager')
882
spy_cfg.IPythonWidget.paging = 'inside'
884
spy_cfg.IPythonWidget.paging = 'none'
887
calltips_o = self.get_option('show_calltips')
888
spy_cfg.IPythonWidget.enable_calltips = calltips_o
891
buffer_size_o = self.get_option('buffer_size')
892
spy_cfg.IPythonWidget.buffer_size = buffer_size_o
895
in_prompt_o = self.get_option('in_prompt')
896
out_prompt_o = self.get_option('out_prompt')
898
spy_cfg.IPythonWidget.in_prompt = in_prompt_o
900
spy_cfg.IPythonWidget.out_prompt = out_prompt_o
902
# Merge IPython and Spyder configs. Spyder prefs will have prevalence
904
ip_cfg._merge(spy_cfg)
907
def initialize_application(self):
908
"""Initialize IPython application"""
909
#======================================================================
910
# For IPython developers review [1]
911
self.ipython_app = IPythonApp()
912
# Is the following line really necessary?
913
#self.ipython_app.initialize_all_except_qt()
914
#======================================================================
916
def register_client(self, connection_file, kernel_widget_id, client_name):
917
"""Register new IPython client"""
918
#======================================================================
919
# For IPython developers review [2]
920
ipywidget = self.ipython_app.new_ipywidget(connection_file,
921
config=self.ipywidget_config())
922
#======================================================================
924
client = IPythonClient(self, connection_file, kernel_widget_id,
925
client_name, ipywidget,
926
history_filename='.history.py',
927
menu_actions=self.menu_actions)
929
control = client.ipywidget._control
930
page_control = client.ipywidget._page_control
933
self.connect(control, SIGNAL("go_to_error(QString)"), self.go_to_error)
935
# Handle kernel interrupts
936
extconsoles = self.main.extconsole.shellwidgets
939
if extconsoles[-1].connection_file == connection_file:
940
kernel_widget = extconsoles[-1]
941
ipywidget.custom_interrupt_requested.connect(
942
kernel_widget.keyboard_interrupt)
943
if kernel_widget is None:
944
ipywidget.custom_interrupt_requested.connect(
945
client.interrupt_message)
947
# Handle kernel restarts asked by the user
948
if kernel_widget is not None:
949
ipywidget.custom_restart_requested.connect(self.create_new_kernel)
951
ipywidget.custom_restart_requested.connect(client.restart_message)
953
# Print a message if kernel dies unexpectedly
954
ipywidget.custom_restart_kernel_died.connect(
955
lambda t: client.if_kernel_dies(t))
957
# Connect text widget to our inspector
958
if kernel_widget is not None and self.inspector is not None:
959
control.set_inspector(self.inspector)
961
# Connect client to our history log
962
if self.historylog is not None:
963
self.historylog.add_history(client.history_filename)
964
self.connect(client, SIGNAL('append_to_history(QString,QString)'),
965
self.historylog.append_to_history)
967
# Apply settings to newly created client widget:
968
client.set_font( self.get_plugin_font() )
970
# Add tab and connect focus signals to client's control widgets
971
self.add_tab(client, name=client.get_name())
972
self.connect(control, SIGNAL('focus_changed()'),
973
lambda: self.emit(SIGNAL('focus_changed()')))
974
self.connect(page_control, SIGNAL('focus_changed()'),
975
lambda: self.emit(SIGNAL('focus_changed()')))
977
# Update the find widget if focus changes between control and
979
self.find_widget.set_editor(control)
981
self.connect(control, SIGNAL('visibility_changed(bool)'),
983
self.connect(page_control, SIGNAL('visibility_changed(bool)'),
985
self.connect(page_control, SIGNAL('show_find_widget()'),
986
self.find_widget.show)
988
def close_related_ipyclients(self, client):
989
"""Close all IPython clients related to *client*, except itself"""
990
for cl in self.clients[:]:
991
if cl is not client and \
992
cl.connection_file == client.connection_file:
993
self.close_console(client=cl)
995
def get_ipywidget_by_kernelwidget_id(self, kernel_id):
996
"""Return the IPython widget associated to a kernel widget id"""
997
for cl in self.clients:
998
if cl.kernel_widget_id == kernel_id:
1001
raise ValueError, "Unknown kernel widget ID %r" % kernel_id
1003
def add_tab(self, widget, name):
1005
self.clients.append(widget)
1006
index = self.tabwidget.addTab(widget, get_icon('ipython_console.png'),
1008
self.tabwidget.setCurrentIndex(index)
1009
if self.dockwidget and not self.ismaximized:
1010
self.dockwidget.setVisible(True)
1011
self.dockwidget.raise_()
1012
self.activateWindow()
1013
widget.get_control().setFocus()
1015
def move_tab(self, index_from, index_to):
1017
Move tab (tabs themselves have already been moved by the tabwidget)
1019
client = self.clients.pop(index_from)
1020
self.clients.insert(index_to, client)
1021
self.emit(SIGNAL('update_plugin_title()'))
1023
def close_console(self, index=None, client=None, force=False):
1024
"""Close console tab from index or widget (or close current tab)"""
1025
if not self.tabwidget.count():
1027
if client is not None:
1028
index = self.tabwidget.indexOf(client)
1029
if index is None and client is None:
1030
index = self.tabwidget.currentIndex()
1031
if index is not None:
1032
client = self.tabwidget.widget(index)
1034
# Check if related clients or kernels are opened
1035
# and eventually ask before closing them
1036
if not force and isinstance(client, IPythonClient):
1037
extconsole = self.main.extconsole
1038
idx = extconsole.get_shell_index_from_id(client.kernel_widget_id)
1041
if self.get_option('ask_before_closing'):
1042
ans = QMessageBox.question(self, self.get_plugin_title(),
1043
_("%s will be closed.\n"
1044
"Do you want to kill the associated kernel "
1045
"and all of its clients?") % client.get_name(),
1046
QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
1047
if ans == QMessageBox.Cancel:
1049
close_all = ans == QMessageBox.Yes
1051
extconsole.close_console(index=idx, from_ipyclient=True)
1052
self.close_related_ipyclients(client)
1055
# Note: client index may have changed after closing related widgets
1056
self.tabwidget.removeTab(self.tabwidget.indexOf(client))
1057
self.clients.remove(client)
1059
self.emit(SIGNAL('update_plugin_title()'))
1061
def go_to_error(self, text):
1062
"""Go to error if relevant"""
1063
match = get_error_match(unicode(text))
1065
fname, lnb = match.groups()
1066
self.emit(SIGNAL("edit_goto(QString,int,QString)"),
1067
osp.abspath(fname), int(lnb), '')
1069
def get_client_index_from_id(self, client_id):
1070
"""Return client index from id"""
1071
for index, client in enumerate(self.clients):
1072
if id(client) == client_id:
1075
def rename_ipyclient_tab(self, connection_file, client_widget_id):
1076
"""Add the pid of the kernel process to an IPython client tab"""
1077
index = self.get_client_index_from_id(client_widget_id)
1078
match = re.match('^kernel-(\d+).json', connection_file)
1079
if match is not None: # should not fail, but we never know...
1080
name = _("Console") + " " + match.groups()[0] + '/' + chr(65)
1081
self.tabwidget.setTabText(index, name)
1083
def create_new_kernel(self):
1084
"""Create a new kernel if the user asks for it"""
1085
# Took this bit of code (until if result == ) from the IPython project
1086
# (frontend/qt/frontend_widget.py - restart_kernel).
1087
# Licensed under the BSD license
1088
message = _('Are you sure you want to restart the kernel?')
1089
buttons = QMessageBox.Yes | QMessageBox.No
1090
result = QMessageBox.question(self, _('Restart kernel?'),
1092
if result == QMessageBox.Yes:
1093
extconsole = self.main.extconsole
1094
extconsole.start_ipykernel(create_client=False)
1095
kernel_widget = extconsole.shellwidgets[-1]
1096
self.connect(kernel_widget,
1097
SIGNAL('create_ipython_client(QString)'),
1098
lambda cf: self.connect_to_new_kernel(cf, kernel_widget))
1100
def connect_to_new_kernel(self, connection_file, kernel_widget):
1102
After a new kernel is created, execute this action to connect the new
1103
kernel to the old client
1105
extconsole = self.main.extconsole
1106
client = self.tabwidget.currentWidget()
1108
# Close old kernel tab
1109
idx = extconsole.get_shell_index_from_id(client.kernel_widget_id)
1110
extconsole.close_console(index=idx, from_ipyclient=True)
1112
# Set attributes for the new kernel
1113
extconsole.set_ipykernel_attrs(connection_file, kernel_widget)
1115
# Connect client to new kernel
1116
kernel_manager = self.ipython_app.create_kernel_manager(connection_file)
1117
client.ipywidget.kernel_manager = kernel_manager
1118
client.kernel_widget_id = id(kernel_widget)
1119
client.get_control().setFocus()
1122
client_widget_id = id(client)
1123
self.rename_ipyclient_tab(connection_file, client_widget_id)
1126
#TODO: try and reimplement this block
1127
# (this is still the original code block copied from externalconsole.py)
1128
# def dragEnterEvent(self, event):
1129
# """Reimplement Qt method
1130
# Inform Qt about the types of data that the widget accepts"""
1131
# source = event.mimeData()
1132
# if source.hasUrls():
1133
# if mimedata2url(source):
1134
# pathlist = mimedata2url(source)
1135
# shellwidget = self.tabwidget.currentWidget()
1136
# if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1137
# event.acceptProposedAction()
1138
# elif shellwidget is None or not shellwidget.is_running():
1141
# event.acceptProposedAction()
1144
# elif source.hasText():
1145
# event.acceptProposedAction()
1147
# def dropEvent(self, event):
1148
# """Reimplement Qt method
1149
# Unpack dropped data and handle it"""
1150
# source = event.mimeData()
1151
# shellwidget = self.tabwidget.currentWidget()
1152
# if source.hasText():
1153
# qstr = source.text()
1154
# if is_python_script(unicode(qstr)):
1157
# shellwidget.shell.insert_text(qstr)
1158
# elif source.hasUrls():
1159
# pathlist = mimedata2url(source)
1160
# if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1161
# for fname in pathlist:
1164
# shellwidget.shell.drop_pathlist(pathlist)
1165
# event.acceptProposedAction()