1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7""" 8IPython Console plugin based on QtConsole 9""" 10 11# pylint: disable=C0103 12# pylint: disable=R0903 13# pylint: disable=R0911 14# pylint: disable=R0201 15 16# Standard library imports 17import atexit 18import codecs 19import os 20import os.path as osp 21import uuid 22import sys 23 24# Third party imports 25from jupyter_client.connect import find_connection_file 26from jupyter_core.paths import jupyter_config_dir, jupyter_runtime_dir 27from qtconsole.client import QtKernelClient 28from qtconsole.manager import QtKernelManager 29from qtpy import PYQT5 30from qtpy.compat import getopenfilename 31from qtpy.QtCore import Qt, Signal, Slot 32from qtpy.QtWidgets import (QApplication, QCheckBox, QDialog, QDialogButtonBox, 33 QFormLayout, QGridLayout, QGroupBox, QHBoxLayout, 34 QLabel, QLineEdit, QMessageBox, QPushButton, 35 QTabWidget, QVBoxLayout, QWidget) 36from traitlets.config.loader import Config, load_pyconfig_files 37from zmq.ssh import tunnel as zmqtunnel 38try: 39 import pexpect 40except ImportError: 41 pexpect = None 42 43# Local imports 44from spyder import dependencies 45from spyder.config.base import (_, DEV, get_conf_path, get_home_dir, 46 get_module_path, PYTEST) 47from spyder.config.main import CONF 48from spyder.plugins import SpyderPluginWidget 49from spyder.plugins.configdialog import PluginConfigPage 50from spyder.py3compat import is_string, PY2, to_text_string 51from spyder.utils.ipython.kernelspec import SpyderKernelSpec 52from spyder.utils.ipython.style import create_qss_style 53from spyder.utils.qthelpers import create_action, MENU_SEPARATOR 54from spyder.utils import icon_manager as ima 55from spyder.utils import encoding, programs, sourcecode 56from spyder.utils.programs import TEMPDIR 57from spyder.utils.misc import get_error_match, remove_backslashes 58from spyder.widgets.findreplace import FindReplace 59from spyder.widgets.ipythonconsole import ClientWidget 60from spyder.widgets.tabs import Tabs 61 62 63# Dependencies 64SYMPY_REQVER = '>=0.7.3' 65dependencies.add("sympy", _("Symbolic mathematics in the IPython Console"), 66 required_version=SYMPY_REQVER, optional=True) 67 68CYTHON_REQVER = '>=0.21' 69dependencies.add("cython", _("Run Cython files in the IPython Console"), 70 required_version=CYTHON_REQVER, optional=True) 71 72QTCONSOLE_REQVER = ">=4.2.0" 73dependencies.add("qtconsole", _("Integrate the IPython console"), 74 required_version=QTCONSOLE_REQVER) 75 76IPYTHON_REQVER = ">=4.0;<6.0" if PY2 else ">=4.0" 77dependencies.add("IPython", _("IPython interactive python environment"), 78 required_version=IPYTHON_REQVER) 79 80#------------------------------------------------------------------------------ 81# Existing kernels 82#------------------------------------------------------------------------------ 83# Replacing pyzmq openssh_tunnel method to work around the issue 84# https://github.com/zeromq/pyzmq/issues/589 which was solved in pyzmq 85# https://github.com/zeromq/pyzmq/pull/615 86def _stop_tunnel(cmd): 87 pexpect.run(cmd) 88 89def openssh_tunnel(self, lport, rport, server, remoteip='127.0.0.1', 90 keyfile=None, password=None, timeout=0.4): 91 if pexpect is None: 92 raise ImportError("pexpect unavailable, use paramiko_tunnel") 93 ssh="ssh " 94 if keyfile: 95 ssh += "-i " + keyfile 96 97 if ':' in server: 98 server, port = server.split(':') 99 ssh += " -p %s" % port 100 101 cmd = "%s -O check %s" % (ssh, server) 102 (output, exitstatus) = pexpect.run(cmd, withexitstatus=True) 103 if not exitstatus: 104 pid = int(output[output.find("(pid=")+5:output.find(")")]) 105 cmd = "%s -O forward -L 127.0.0.1:%i:%s:%i %s" % ( 106 ssh, lport, remoteip, rport, server) 107 (output, exitstatus) = pexpect.run(cmd, withexitstatus=True) 108 if not exitstatus: 109 atexit.register(_stop_tunnel, cmd.replace("-O forward", 110 "-O cancel", 111 1)) 112 return pid 113 cmd = "%s -f -S none -L 127.0.0.1:%i:%s:%i %s sleep %i" % ( 114 ssh, lport, remoteip, rport, server, timeout) 115 116 # pop SSH_ASKPASS from env 117 env = os.environ.copy() 118 env.pop('SSH_ASKPASS', None) 119 120 ssh_newkey = 'Are you sure you want to continue connecting' 121 tunnel = pexpect.spawn(cmd, env=env) 122 failed = False 123 while True: 124 try: 125 i = tunnel.expect([ssh_newkey, '[Pp]assword:'], timeout=.1) 126 if i==0: 127 host = server.split('@')[-1] 128 question = _("The authenticity of host <b>%s</b> can't be " 129 "established. Are you sure you want to continue " 130 "connecting?") % host 131 reply = QMessageBox.question(self, _('Warning'), question, 132 QMessageBox.Yes | QMessageBox.No, 133 QMessageBox.No) 134 if reply == QMessageBox.Yes: 135 tunnel.sendline('yes') 136 continue 137 else: 138 tunnel.sendline('no') 139 raise RuntimeError( 140 _("The authenticity of the host can't be established")) 141 if i==1 and password is not None: 142 tunnel.sendline(password) 143 except pexpect.TIMEOUT: 144 continue 145 except pexpect.EOF: 146 if tunnel.exitstatus: 147 raise RuntimeError(_("Tunnel '%s' failed to start") % cmd) 148 else: 149 return tunnel.pid 150 else: 151 if failed or password is None: 152 raise RuntimeError(_("Could not connect to remote host")) 153 # TODO: Use this block when pyzmq bug #620 is fixed 154 # # Prompt a passphrase dialog to the user for a second attempt 155 # password, ok = QInputDialog.getText(self, _('Password'), 156 # _('Enter password for: ') + server, 157 # echo=QLineEdit.Password) 158 # if ok is False: 159 # raise RuntimeError('Could not connect to remote host.') 160 tunnel.sendline(password) 161 failed = True 162 163 164class KernelConnectionDialog(QDialog): 165 """Dialog to connect to existing kernels (either local or remote)""" 166 167 def __init__(self, parent=None): 168 super(KernelConnectionDialog, self).__init__(parent) 169 self.setWindowTitle(_('Connect to an existing kernel')) 170 171 main_label = QLabel(_("Please enter the connection info of the kernel " 172 "you want to connect to. For that you can " 173 "either select its JSON connection file using " 174 "the <tt>Browse</tt> button, or write directly " 175 "its id, in case it's a local kernel (for " 176 "example <tt>kernel-3764.json</tt> or just " 177 "<tt>3764</tt>).")) 178 main_label.setWordWrap(True) 179 main_label.setAlignment(Qt.AlignJustify) 180 181 # connection file 182 cf_label = QLabel(_('Connection info:')) 183 self.cf = QLineEdit() 184 self.cf.setPlaceholderText(_('Path to connection file or kernel id')) 185 self.cf.setMinimumWidth(250) 186 cf_open_btn = QPushButton(_('Browse')) 187 cf_open_btn.clicked.connect(self.select_connection_file) 188 189 cf_layout = QHBoxLayout() 190 cf_layout.addWidget(cf_label) 191 cf_layout.addWidget(self.cf) 192 cf_layout.addWidget(cf_open_btn) 193 194 # remote kernel checkbox 195 self.rm_cb = QCheckBox(_('This is a remote kernel')) 196 197 # ssh connection 198 self.hn = QLineEdit() 199 self.hn.setPlaceholderText(_('username@hostname:port')) 200 201 self.kf = QLineEdit() 202 self.kf.setPlaceholderText(_('Path to ssh key file')) 203 kf_open_btn = QPushButton(_('Browse')) 204 kf_open_btn.clicked.connect(self.select_ssh_key) 205 206 kf_layout = QHBoxLayout() 207 kf_layout.addWidget(self.kf) 208 kf_layout.addWidget(kf_open_btn) 209 210 self.pw = QLineEdit() 211 self.pw.setPlaceholderText(_('Password or ssh key passphrase')) 212 self.pw.setEchoMode(QLineEdit.Password) 213 214 ssh_form = QFormLayout() 215 ssh_form.addRow(_('Host name'), self.hn) 216 ssh_form.addRow(_('Ssh key'), kf_layout) 217 ssh_form.addRow(_('Password'), self.pw) 218 219 # Ok and Cancel buttons 220 self.accept_btns = QDialogButtonBox( 221 QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 222 Qt.Horizontal, self) 223 224 self.accept_btns.accepted.connect(self.accept) 225 self.accept_btns.rejected.connect(self.reject) 226 227 # Dialog layout 228 layout = QVBoxLayout(self) 229 layout.addWidget(main_label) 230 layout.addLayout(cf_layout) 231 layout.addWidget(self.rm_cb) 232 layout.addLayout(ssh_form) 233 layout.addWidget(self.accept_btns) 234 235 # remote kernel checkbox enables the ssh_connection_form 236 def ssh_set_enabled(state): 237 for wid in [self.hn, self.kf, kf_open_btn, self.pw]: 238 wid.setEnabled(state) 239 for i in range(ssh_form.rowCount()): 240 ssh_form.itemAt(2 * i).widget().setEnabled(state) 241 242 ssh_set_enabled(self.rm_cb.checkState()) 243 self.rm_cb.stateChanged.connect(ssh_set_enabled) 244 245 def select_connection_file(self): 246 cf = getopenfilename(self, _('Open connection file'), 247 jupyter_runtime_dir(), '*.json;;*.*')[0] 248 self.cf.setText(cf) 249 250 def select_ssh_key(self): 251 kf = getopenfilename(self, _('Select ssh key'), 252 get_home_dir(), '*.pem;;*.*')[0] 253 self.kf.setText(kf) 254 255 @staticmethod 256 def get_connection_parameters(parent=None, dialog=None): 257 if not dialog: 258 dialog = KernelConnectionDialog(parent) 259 result = dialog.exec_() 260 is_remote = bool(dialog.rm_cb.checkState()) 261 accepted = result == QDialog.Accepted 262 if is_remote: 263 falsy_to_none = lambda arg: arg if arg else None 264 return (dialog.cf.text(), # connection file 265 falsy_to_none(dialog.hn.text()), # host name 266 falsy_to_none(dialog.kf.text()), # ssh key file 267 falsy_to_none(dialog.pw.text()), # ssh password 268 accepted) # ok 269 else: 270 path = dialog.cf.text() 271 _dir, filename = osp.dirname(path), osp.basename(path) 272 if _dir == '' and not filename.endswith('.json'): 273 path = osp.join(jupyter_runtime_dir(), 'kernel-'+path+'.json') 274 return (path, None, None, None, accepted) 275 276 277#------------------------------------------------------------------------------ 278# Config page 279#------------------------------------------------------------------------------ 280class IPythonConsoleConfigPage(PluginConfigPage): 281 282 def __init__(self, plugin, parent): 283 PluginConfigPage.__init__(self, plugin, parent) 284 self.get_name = lambda: _("IPython console") 285 286 def setup_page(self): 287 newcb = self.create_checkbox 288 289 # Interface Group 290 interface_group = QGroupBox(_("Interface")) 291 banner_box = newcb(_("Display initial banner"), 'show_banner', 292 tip=_("This option lets you hide the message shown at\n" 293 "the top of the console when it's opened.")) 294 pager_box = newcb(_("Use a pager to display additional text inside " 295 "the console"), 'use_pager', 296 tip=_("Useful if you don't want to fill the " 297 "console with long help or completion texts.\n" 298 "Note: Use the Q key to get out of the " 299 "pager.")) 300 calltips_box = newcb(_("Display balloon tips"), 'show_calltips') 301 ask_box = newcb(_("Ask for confirmation before closing"), 302 'ask_before_closing') 303 reset_namespace_box = newcb( 304 _("Ask for confirmation before removing all user-defined " 305 "variables"), 306 'show_reset_namespace_warning', 307 tip=_("This option lets you hide the warning message shown\n" 308 "when resetting the namespace from Spyder.")) 309 show_time_box = newcb(_("Show elapsed time"), 'show_elapsed_time') 310 311 interface_layout = QVBoxLayout() 312 interface_layout.addWidget(banner_box) 313 interface_layout.addWidget(pager_box) 314 interface_layout.addWidget(calltips_box) 315 interface_layout.addWidget(ask_box) 316 interface_layout.addWidget(reset_namespace_box) 317 interface_layout.addWidget(show_time_box) 318 interface_group.setLayout(interface_layout) 319 320 comp_group = QGroupBox(_("Completion Type")) 321 comp_label = QLabel(_("Decide what type of completion to use")) 322 comp_label.setWordWrap(True) 323 completers = [(_("Graphical"), 0), (_("Terminal"), 1), (_("Plain"), 2)] 324 comp_box = self.create_combobox(_("Completion:")+" ", completers, 325 'completion_type') 326 comp_layout = QVBoxLayout() 327 comp_layout.addWidget(comp_label) 328 comp_layout.addWidget(comp_box) 329 comp_group.setLayout(comp_layout) 330 331 # Source Code Group 332 source_code_group = QGroupBox(_("Source code")) 333 buffer_spin = self.create_spinbox( 334 _("Buffer: "), _(" lines"), 335 'buffer_size', min_=-1, max_=1000000, step=100, 336 tip=_("Set the maximum number of lines of text shown in the\n" 337 "console before truncation. Specifying -1 disables it\n" 338 "(not recommended!)")) 339 source_code_layout = QVBoxLayout() 340 source_code_layout.addWidget(buffer_spin) 341 source_code_group.setLayout(source_code_layout) 342 343 # --- Graphics --- 344 # Pylab Group 345 pylab_group = QGroupBox(_("Support for graphics (Matplotlib)")) 346 pylab_box = newcb(_("Activate support"), 'pylab') 347 autoload_pylab_box = newcb(_("Automatically load Pylab and NumPy " 348 "modules"), 349 'pylab/autoload', 350 tip=_("This lets you load graphics support " 351 "without importing \nthe commands to do " 352 "plots. Useful to work with other\n" 353 "plotting libraries different to " 354 "Matplotlib or to develop \nGUIs with " 355 "Spyder.")) 356 autoload_pylab_box.setEnabled(self.get_option('pylab')) 357 pylab_box.toggled.connect(autoload_pylab_box.setEnabled) 358 359 pylab_layout = QVBoxLayout() 360 pylab_layout.addWidget(pylab_box) 361 pylab_layout.addWidget(autoload_pylab_box) 362 pylab_group.setLayout(pylab_layout) 363 364 # Pylab backend Group 365 inline = _("Inline") 366 automatic = _("Automatic") 367 backend_group = QGroupBox(_("Graphics backend")) 368 bend_label = QLabel(_("Decide how graphics are going to be displayed " 369 "in the console. If unsure, please select " 370 "<b>%s</b> to put graphics inside the " 371 "console or <b>%s</b> to interact with " 372 "them (through zooming and panning) in a " 373 "separate window.") % (inline, automatic)) 374 bend_label.setWordWrap(True) 375 376 backends = [(inline, 0), (automatic, 1), ("Qt5", 2), ("Qt4", 3)] 377 378 if sys.platform == 'darwin': 379 backends.append( ("OS X", 4) ) 380 if sys.platform.startswith('linux'): 381 backends.append( ("Gtk3", 5) ) 382 backends.append( ("Gtk", 6) ) 383 if PY2: 384 backends.append( ("Wx", 7) ) 385 backends.append( ("Tkinter", 8) ) 386 backends = tuple(backends) 387 388 backend_box = self.create_combobox( _("Backend:")+" ", backends, 389 'pylab/backend', default=0, 390 tip=_("This option will be applied the " 391 "next time a console is opened.")) 392 393 backend_layout = QVBoxLayout() 394 backend_layout.addWidget(bend_label) 395 backend_layout.addWidget(backend_box) 396 backend_group.setLayout(backend_layout) 397 backend_group.setEnabled(self.get_option('pylab')) 398 pylab_box.toggled.connect(backend_group.setEnabled) 399 400 # Inline backend Group 401 inline_group = QGroupBox(_("Inline backend")) 402 inline_label = QLabel(_("Decide how to render the figures created by " 403 "this backend")) 404 inline_label.setWordWrap(True) 405 formats = (("PNG", 0), ("SVG", 1)) 406 format_box = self.create_combobox(_("Format:")+" ", formats, 407 'pylab/inline/figure_format', default=0) 408 resolution_spin = self.create_spinbox( 409 _("Resolution:")+" ", " "+_("dpi"), 410 'pylab/inline/resolution', min_=50, max_=999, step=0.1, 411 tip=_("Only used when the format is PNG. Default is " 412 "72")) 413 width_spin = self.create_spinbox( 414 _("Width:")+" ", " "+_("inches"), 415 'pylab/inline/width', min_=2, max_=20, step=1, 416 tip=_("Default is 6")) 417 height_spin = self.create_spinbox( 418 _("Height:")+" ", " "+_("inches"), 419 'pylab/inline/height', min_=1, max_=20, step=1, 420 tip=_("Default is 4")) 421 422 inline_v_layout = QVBoxLayout() 423 inline_v_layout.addWidget(inline_label) 424 inline_layout = QGridLayout() 425 inline_layout.addWidget(format_box.label, 1, 0) 426 inline_layout.addWidget(format_box.combobox, 1, 1) 427 inline_layout.addWidget(resolution_spin.plabel, 2, 0) 428 inline_layout.addWidget(resolution_spin.spinbox, 2, 1) 429 inline_layout.addWidget(resolution_spin.slabel, 2, 2) 430 inline_layout.addWidget(width_spin.plabel, 3, 0) 431 inline_layout.addWidget(width_spin.spinbox, 3, 1) 432 inline_layout.addWidget(width_spin.slabel, 3, 2) 433 inline_layout.addWidget(height_spin.plabel, 4, 0) 434 inline_layout.addWidget(height_spin.spinbox, 4, 1) 435 inline_layout.addWidget(height_spin.slabel, 4, 2) 436 inline_h_layout = QHBoxLayout() 437 inline_h_layout.addLayout(inline_layout) 438 inline_h_layout.addStretch(1) 439 inline_v_layout.addLayout(inline_h_layout) 440 inline_group.setLayout(inline_v_layout) 441 inline_group.setEnabled(self.get_option('pylab')) 442 pylab_box.toggled.connect(inline_group.setEnabled) 443 444 # --- Startup --- 445 # Run lines Group 446 run_lines_group = QGroupBox(_("Run code")) 447 run_lines_label = QLabel(_("You can run several lines of code when " 448 "a console is started. Please introduce " 449 "each one separated by commas, for " 450 "example:<br>" 451 "<i>import os, import sys</i>")) 452 run_lines_label.setWordWrap(True) 453 run_lines_edit = self.create_lineedit(_("Lines:"), 'startup/run_lines', 454 '', alignment=Qt.Horizontal) 455 456 run_lines_layout = QVBoxLayout() 457 run_lines_layout.addWidget(run_lines_label) 458 run_lines_layout.addWidget(run_lines_edit) 459 run_lines_group.setLayout(run_lines_layout) 460 461 # Run file Group 462 run_file_group = QGroupBox(_("Run a file")) 463 run_file_label = QLabel(_("You can also run a whole file at startup " 464 "instead of just some lines (This is " 465 "similar to have a PYTHONSTARTUP file).")) 466 run_file_label.setWordWrap(True) 467 file_radio = newcb(_("Use the following file:"), 468 'startup/use_run_file', False) 469 run_file_browser = self.create_browsefile('', 'startup/run_file', '') 470 run_file_browser.setEnabled(False) 471 file_radio.toggled.connect(run_file_browser.setEnabled) 472 473 run_file_layout = QVBoxLayout() 474 run_file_layout.addWidget(run_file_label) 475 run_file_layout.addWidget(file_radio) 476 run_file_layout.addWidget(run_file_browser) 477 run_file_group.setLayout(run_file_layout) 478 479 # ---- Advanced settings ---- 480 # Greedy completer group 481 greedy_group = QGroupBox(_("Greedy completion")) 482 greedy_label = QLabel(_("Enable <tt>Tab</tt> completion on elements " 483 "of lists, results of function calls, etc, " 484 "<i>without</i> assigning them to a " 485 "variable.<br>" 486 "For example, you can get completions on " 487 "things like <tt>li[0].<Tab></tt> or " 488 "<tt>ins.meth().<Tab></tt>")) 489 greedy_label.setWordWrap(True) 490 greedy_box = newcb(_("Use the greedy completer"), "greedy_completer", 491 tip="<b>Warning</b>: It can be unsafe because the " 492 "code is actually evaluated when you press " 493 "<tt>Tab</tt>.") 494 495 greedy_layout = QVBoxLayout() 496 greedy_layout.addWidget(greedy_label) 497 greedy_layout.addWidget(greedy_box) 498 greedy_group.setLayout(greedy_layout) 499 500 # Autocall group 501 autocall_group = QGroupBox(_("Autocall")) 502 autocall_label = QLabel(_("Autocall makes IPython automatically call " 503 "any callable object even if you didn't type " 504 "explicit parentheses.<br>" 505 "For example, if you type <i>str 43</i> it " 506 "becomes <i>str(43)</i> automatically.")) 507 autocall_label.setWordWrap(True) 508 509 smart = _('Smart') 510 full = _('Full') 511 autocall_opts = ((_('Off'), 0), (smart, 1), (full, 2)) 512 autocall_box = self.create_combobox( 513 _("Autocall: "), autocall_opts, 'autocall', default=0, 514 tip=_("On <b>%s</b> mode, Autocall is not applied if " 515 "there are no arguments after the callable. On " 516 "<b>%s</b> mode, all callable objects are " 517 "automatically called (even if no arguments are " 518 "present).") % (smart, full)) 519 520 autocall_layout = QVBoxLayout() 521 autocall_layout.addWidget(autocall_label) 522 autocall_layout.addWidget(autocall_box) 523 autocall_group.setLayout(autocall_layout) 524 525 # Sympy group 526 sympy_group = QGroupBox(_("Symbolic Mathematics")) 527 sympy_label = QLabel(_("Perfom symbolic operations in the console " 528 "(e.g. integrals, derivatives, vector calculus, " 529 "etc) and get the outputs in a beautifully " 530 "printed style (it requires the Sympy module).")) 531 sympy_label.setWordWrap(True) 532 sympy_box = newcb(_("Use symbolic math"), "symbolic_math", 533 tip=_("This option loads the Sympy library to work " 534 "with.<br>Please refer to its documentation to " 535 "learn how to use it.")) 536 537 sympy_layout = QVBoxLayout() 538 sympy_layout.addWidget(sympy_label) 539 sympy_layout.addWidget(sympy_box) 540 sympy_group.setLayout(sympy_layout) 541 542 # Prompts group 543 prompts_group = QGroupBox(_("Prompts")) 544 prompts_label = QLabel(_("Modify how Input and Output prompts are " 545 "shown in the console.")) 546 prompts_label.setWordWrap(True) 547 in_prompt_edit = self.create_lineedit(_("Input prompt:"), 548 'in_prompt', '', 549 _('Default is<br>' 550 'In [<span class="in-prompt-number">' 551 '%i</span>]:'), 552 alignment=Qt.Horizontal) 553 out_prompt_edit = self.create_lineedit(_("Output prompt:"), 554 'out_prompt', '', 555 _('Default is<br>' 556 'Out[<span class="out-prompt-number">' 557 '%i</span>]:'), 558 alignment=Qt.Horizontal) 559 560 prompts_layout = QVBoxLayout() 561 prompts_layout.addWidget(prompts_label) 562 prompts_g_layout = QGridLayout() 563 prompts_g_layout.addWidget(in_prompt_edit.label, 0, 0) 564 prompts_g_layout.addWidget(in_prompt_edit.textbox, 0, 1) 565 prompts_g_layout.addWidget(out_prompt_edit.label, 1, 0) 566 prompts_g_layout.addWidget(out_prompt_edit.textbox, 1, 1) 567 prompts_layout.addLayout(prompts_g_layout) 568 prompts_group.setLayout(prompts_layout) 569 570 # --- Tabs organization --- 571 tabs = QTabWidget() 572 tabs.addTab(self.create_tab(interface_group, comp_group, 573 source_code_group), _("Display")) 574 tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group), 575 _("Graphics")) 576 tabs.addTab(self.create_tab(run_lines_group, run_file_group), 577 _("Startup")) 578 tabs.addTab(self.create_tab(greedy_group, autocall_group, sympy_group, 579 prompts_group), _("Advanced Settings")) 580 581 vlayout = QVBoxLayout() 582 vlayout.addWidget(tabs) 583 self.setLayout(vlayout) 584 585 586#------------------------------------------------------------------------------ 587# Plugin widget 588#------------------------------------------------------------------------------ 589class IPythonConsole(SpyderPluginWidget): 590 """ 591 IPython Console plugin 592 593 This is a widget with tabs where each one is a ClientWidget 594 """ 595 CONF_SECTION = 'ipython_console' 596 CONFIGWIDGET_CLASS = IPythonConsoleConfigPage 597 DISABLE_ACTIONS_WHEN_HIDDEN = False 598 599 # Signals 600 focus_changed = Signal() 601 edit_goto = Signal((str, int, str), (str, int, str, bool)) 602 603 # Error messages 604 permission_error_msg = _("The directory {} is not writable and it is " 605 "required to create IPython consoles. Please " 606 "make it writable.") 607 608 def __init__(self, parent, testing=False, test_dir=TEMPDIR, 609 test_no_stderr=False): 610 """Ipython Console constructor.""" 611 if PYQT5: 612 SpyderPluginWidget.__init__(self, parent, main = parent) 613 else: 614 SpyderPluginWidget.__init__(self, parent) 615 616 self.tabwidget = None 617 self.menu_actions = None 618 619 self.help = None # Help plugin 620 self.historylog = None # History log plugin 621 self.variableexplorer = None # Variable explorer plugin 622 self.editor = None # Editor plugin 623 self.projects = None # Projects plugin 624 625 self.master_clients = 0 626 self.clients = [] 627 self.filenames = [] 628 self.mainwindow_close = False 629 self.create_new_client_if_empty = True 630 631 # Attrs for testing 632 self.testing = testing 633 self.test_dir = test_dir 634 self.test_no_stderr = test_no_stderr 635 636 # Initialize plugin 637 if not self.testing: 638 self.initialize_plugin() 639 640 # Create temp dir on testing to save kernel errors 641 if self.testing: 642 if not osp.isdir(osp.join(test_dir)): 643 os.makedirs(osp.join(test_dir)) 644 645 646 layout = QVBoxLayout() 647 self.tabwidget = Tabs(self, self.menu_actions, rename_tabs=True, 648 split_char='/', split_index=0) 649 if hasattr(self.tabwidget, 'setDocumentMode')\ 650 and not sys.platform == 'darwin': 651 # Don't set document mode to true on OSX because it generates 652 # a crash when the console is detached from the main window 653 # Fixes Issue 561 654 self.tabwidget.setDocumentMode(True) 655 self.tabwidget.currentChanged.connect(self.refresh_plugin) 656 self.tabwidget.tabBar().tabMoved.connect(self.move_tab) 657 self.tabwidget.tabBar().sig_change_name.connect( 658 self.rename_tabs_after_change) 659 660 self.tabwidget.set_close_function(self.close_client) 661 662 if sys.platform == 'darwin': 663 tab_container = QWidget() 664 tab_container.setObjectName('tab-container') 665 tab_layout = QHBoxLayout(tab_container) 666 tab_layout.setContentsMargins(0, 0, 0, 0) 667 tab_layout.addWidget(self.tabwidget) 668 layout.addWidget(tab_container) 669 else: 670 layout.addWidget(self.tabwidget) 671 672 # Find/replace widget 673 self.find_widget = FindReplace(self) 674 self.find_widget.hide() 675 if not self.testing: 676 self.register_widget_shortcuts(self.find_widget) 677 layout.addWidget(self.find_widget) 678 679 self.setLayout(layout) 680 681 # Accepting drops 682 self.setAcceptDrops(True) 683 684 #------ SpyderPluginMixin API --------------------------------------------- 685 def update_font(self): 686 """Update font from Preferences""" 687 font = self.get_plugin_font() 688 for client in self.clients: 689 client.set_font(font) 690 691 def apply_plugin_settings(self, options): 692 """Apply configuration file's plugin settings""" 693 font_n = 'plugin_font' 694 font_o = self.get_plugin_font() 695 help_n = 'connect_to_oi' 696 help_o = CONF.get('help', 'connect/ipython_console') 697 color_scheme_n = 'color_scheme_name' 698 color_scheme_o = CONF.get('color_schemes', 'selected') 699 show_time_n = 'show_elapsed_time' 700 show_time_o = self.get_option(show_time_n) 701 reset_namespace_n = 'show_reset_namespace_warning' 702 reset_namespace_o = self.get_option(reset_namespace_n) 703 for client in self.clients: 704 control = client.get_control() 705 if font_n in options: 706 client.set_font(font_o) 707 if help_n in options and control is not None: 708 control.set_help_enabled(help_o) 709 if color_scheme_n in options: 710 client.set_color_scheme(color_scheme_o) 711 if show_time_n in options: 712 client.show_time_action.setChecked(show_time_o) 713 client.set_elapsed_time_visible(show_time_o) 714 if reset_namespace_n in options: 715 client.reset_warning = reset_namespace_o 716 717 def toggle_view(self, checked): 718 """Toggle view""" 719 if checked: 720 self.dockwidget.show() 721 self.dockwidget.raise_() 722 # Start a client in case there are none shown 723 if not self.clients: 724 if self.main.is_setting_up: 725 self.create_new_client(give_focus=False) 726 else: 727 self.create_new_client(give_focus=True) 728 else: 729 self.dockwidget.hide() 730 731 #------ SpyderPluginWidget API -------------------------------------------- 732 def get_plugin_title(self): 733 """Return widget title""" 734 return _('IPython console') 735 736 def get_plugin_icon(self): 737 """Return widget icon""" 738 return ima.icon('ipython_console') 739 740 def get_focus_widget(self): 741 """ 742 Return the widget to give focus to when 743 this plugin's dockwidget is raised on top-level 744 """ 745 client = self.tabwidget.currentWidget() 746 if client is not None: 747 return client.get_control() 748 749 def closing_plugin(self, cancelable=False): 750 """Perform actions before parent main window is closed""" 751 self.mainwindow_close = True 752 for client in self.clients: 753 client.shutdown() 754 client.close() 755 return True 756 757 def refresh_plugin(self): 758 """Refresh tabwidget""" 759 client = None 760 if self.tabwidget.count(): 761 # Give focus to the control widget of the selected tab 762 client = self.tabwidget.currentWidget() 763 control = client.get_control() 764 control.setFocus() 765 buttons = [[b, -7] for b in client.get_toolbar_buttons()] 766 buttons = sum(buttons, [])[:-1] 767 widgets = [client.create_time_label()] + buttons 768 else: 769 control = None 770 widgets = [] 771 self.find_widget.set_editor(control) 772 self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets}) 773 if client and not self.testing: 774 sw = client.shellwidget 775 self.variableexplorer.set_shellwidget_from_id(id(sw)) 776 self.help.set_shell(sw) 777 self.update_tabs_text() 778 self.update_plugin_title.emit() 779 780 def get_plugin_actions(self): 781 """Return a list of actions related to plugin.""" 782 create_client_action = create_action( 783 self, 784 _("Open an &IPython console"), 785 icon=ima.icon('ipython_console'), 786 triggered=self.create_new_client, 787 context=Qt.WidgetWithChildrenShortcut) 788 self.register_shortcut(create_client_action, context="ipython_console", 789 name="New tab") 790 791 restart_action = create_action(self, _("Restart kernel"), 792 icon=ima.icon('restart'), 793 triggered=self.restart_kernel, 794 context=Qt.WidgetWithChildrenShortcut) 795 self.register_shortcut(restart_action, context="ipython_console", 796 name="Restart kernel") 797 798 connect_to_kernel_action = create_action(self, 799 _("Connect to an existing kernel"), None, None, 800 _("Open a new IPython console connected to an existing kernel"), 801 triggered=self.create_client_for_kernel) 802 803 rename_tab_action = create_action(self, _("Rename tab"), 804 icon=ima.icon('rename'), 805 triggered=self.tab_name_editor) 806 807 # Add the action to the 'Consoles' menu on the main window 808 main_consoles_menu = self.main.consoles_menu_actions 809 main_consoles_menu.insert(0, create_client_action) 810 main_consoles_menu += [MENU_SEPARATOR, restart_action, 811 connect_to_kernel_action] 812 813 # Plugin actions 814 self.menu_actions = [create_client_action, MENU_SEPARATOR, 815 restart_action, connect_to_kernel_action, 816 MENU_SEPARATOR, rename_tab_action] 817 818 return self.menu_actions 819 820 def register_plugin(self): 821 """Register plugin in Spyder's main window""" 822 self.main.add_dockwidget(self) 823 824 self.help = self.main.help 825 self.historylog = self.main.historylog 826 self.variableexplorer = self.main.variableexplorer 827 self.editor = self.main.editor 828 self.explorer = self.main.explorer 829 self.projects = self.main.projects 830 831 self.focus_changed.connect(self.main.plugin_focus_changed) 832 self.edit_goto.connect(self.editor.load) 833 self.edit_goto[str, int, str, bool].connect( 834 lambda fname, lineno, word, processevents: 835 self.editor.load(fname, lineno, word, 836 processevents=processevents)) 837 self.editor.breakpoints_saved.connect(self.set_spyder_breakpoints) 838 self.editor.run_in_current_ipyclient.connect(self.run_script) 839 self.main.workingdirectory.set_current_console_wd.connect( 840 self.set_current_client_working_directory) 841 842 self.tabwidget.currentChanged.connect(self.update_working_directory) 843 844 self.explorer.open_interpreter.connect(self.create_client_from_path) 845 self.explorer.run.connect(lambda fname: self.run_script( 846 fname, osp.dirname(fname), '', False, False, False, True)) 847 self.projects.open_interpreter.connect(self.create_client_from_path) 848 self.projects.run.connect(lambda fname: self.run_script( 849 fname, osp.dirname(fname), '', False, False, False, True)) 850 851 #------ Public API (for clients) ------------------------------------------ 852 def get_clients(self): 853 """Return clients list""" 854 return [cl for cl in self.clients if isinstance(cl, ClientWidget)] 855 856 def get_focus_client(self): 857 """Return current client with focus, if any""" 858 widget = QApplication.focusWidget() 859 for client in self.get_clients(): 860 if widget is client or widget is client.get_control(): 861 return client 862 863 def get_current_client(self): 864 """Return the currently selected client""" 865 client = self.tabwidget.currentWidget() 866 if client is not None: 867 return client 868 869 def get_current_shellwidget(self): 870 """Return the shellwidget of the current client""" 871 client = self.get_current_client() 872 if client is not None: 873 return client.shellwidget 874 875 def run_script(self, filename, wdir, args, debug, post_mortem, 876 current_client, clear_variables): 877 """Run script in current or dedicated client""" 878 norm = lambda text: remove_backslashes(to_text_string(text)) 879 880 # Select client to execute code on it 881 is_new_client = False 882 if current_client: 883 client = self.get_current_client() 884 else: 885 client = self.get_client_for_file(filename) 886 if client is None: 887 self.create_client_for_file(filename) 888 client = self.get_current_client() 889 is_new_client = True 890 891 if client is not None: 892 # Internal kernels, use runfile 893 if client.get_kernel() is not None: 894 line = "%s('%s'" % ('debugfile' if debug else 'runfile', 895 norm(filename)) 896 if args: 897 line += ", args='%s'" % norm(args) 898 if wdir: 899 line += ", wdir='%s'" % norm(wdir) 900 if post_mortem: 901 line += ", post_mortem=True" 902 line += ")" 903 else: # External kernels, use %run 904 line = "%run " 905 if debug: 906 line += "-d " 907 line += "\"%s\"" % to_text_string(filename) 908 if args: 909 line += " %s" % norm(args) 910 try: 911 if current_client: 912 self.execute_code(line, current_client, clear_variables) 913 else: 914 if is_new_client: 915 client.shellwidget.silent_execute('%clear') 916 else: 917 client.shellwidget.execute('%clear') 918 client.shellwidget.sig_prompt_ready.connect( 919 lambda: self.execute_code(line, current_client, 920 clear_variables)) 921 except AttributeError: 922 pass 923 self.visibility_changed(True) 924 self.raise_() 925 else: 926 #XXX: not sure it can really happen 927 QMessageBox.warning(self, _('Warning'), 928 _("No IPython console is currently available to run <b>%s</b>." 929 "<br><br>Please open a new one and try again." 930 ) % osp.basename(filename), QMessageBox.Ok) 931 932 def set_current_client_working_directory(self, directory): 933 """Set current client working directory.""" 934 shellwidget = self.get_current_shellwidget() 935 if shellwidget is not None: 936 shellwidget.set_cwd(directory) 937 938 def set_working_directory(self, dirname): 939 """Set current working directory. 940 In the workingdirectory and explorer plugins. 941 """ 942 if dirname and not self.testing: 943 self.main.workingdirectory.chdir(dirname, refresh_explorer=True, 944 refresh_console=False) 945 946 def update_working_directory(self): 947 """Update working directory to console cwd.""" 948 shellwidget = self.get_current_shellwidget() 949 if shellwidget is not None: 950 shellwidget.get_cwd() 951 952 def execute_code(self, lines, current_client=True, clear_variables=False): 953 """Execute code instructions.""" 954 sw = self.get_current_shellwidget() 955 if sw is not None: 956 if sw._reading: 957 pass 958 else: 959 if not current_client: 960 # Clear console and reset namespace for 961 # dedicated clients 962 # See issue 5748 963 try: 964 sw.sig_prompt_ready.disconnect() 965 except TypeError: 966 pass 967 sw.reset_namespace(warning=False, silent=True) 968 elif current_client and clear_variables: 969 sw.reset_namespace(warning=False, silent=True) 970 # Needed to handle an error when kernel_client is none 971 # See issue 6308 972 try: 973 sw.execute(to_text_string(lines)) 974 except AttributeError: 975 pass 976 self.activateWindow() 977 self.get_current_client().get_control().setFocus() 978 979 def write_to_stdin(self, line): 980 sw = self.get_current_shellwidget() 981 if sw is not None: 982 sw.write_to_stdin(line) 983 984 @Slot() 985 @Slot(bool) 986 @Slot(str) 987 @Slot(bool, str) 988 def create_new_client(self, give_focus=True, filename=''): 989 """Create a new client""" 990 self.master_clients += 1 991 client_id = dict(int_id=to_text_string(self.master_clients), 992 str_id='A') 993 cf = self._new_connection_file() 994 show_elapsed_time = self.get_option('show_elapsed_time') 995 reset_warning = self.get_option('show_reset_namespace_warning') 996 client = ClientWidget(self, id_=client_id, 997 history_filename=get_conf_path('history.py'), 998 config_options=self.config_options(), 999 additional_options=self.additional_options(), 1000 interpreter_versions=self.interpreter_versions(), 1001 connection_file=cf, 1002 menu_actions=self.menu_actions, 1003 show_elapsed_time=show_elapsed_time, 1004 reset_warning=reset_warning) 1005 if self.testing: 1006 client.stderr_dir = self.test_dir 1007 self.add_tab(client, name=client.get_name(), filename=filename) 1008 1009 if cf is None: 1010 error_msg = self.permission_error_msg.format(jupyter_runtime_dir()) 1011 client.show_kernel_error(error_msg) 1012 return 1013 1014 # Check if ipykernel is present in the external interpreter. 1015 # Else we won't be able to create a client 1016 if not CONF.get('main_interpreter', 'default'): 1017 pyexec = CONF.get('main_interpreter', 'executable') 1018 has_ipykernel = programs.is_module_installed('ipykernel', 1019 interpreter=pyexec) 1020 has_cloudpickle = programs.is_module_installed('cloudpickle', 1021 interpreter=pyexec) 1022 if not (has_ipykernel and has_cloudpickle): 1023 client.show_kernel_error(_("Your Python environment or " 1024 "installation doesn't " 1025 "have the <tt>ipykernel</tt> and " 1026 "<tt>cloudpickle</tt> modules " 1027 "installed on it. Without these modules " 1028 "is not possible for Spyder to create a " 1029 "console for you.<br><br>" 1030 "You can install them by running " 1031 "in a system terminal:<br><br>" 1032 "<tt>pip install ipykernel cloudpickle</tt>" 1033 "<br><br>" 1034 "or<br><br>" 1035 "<tt>conda install ipykernel cloudpickle</tt>")) 1036 return 1037 1038 self.connect_client_to_kernel(client) 1039 if client.shellwidget.kernel_manager is None: 1040 return 1041 self.register_client(client) 1042 1043 @Slot() 1044 def create_client_for_kernel(self): 1045 """Create a client connected to an existing kernel""" 1046 connect_output = KernelConnectionDialog.get_connection_parameters(self) 1047 (connection_file, hostname, sshkey, password, ok) = connect_output 1048 if not ok: 1049 return 1050 else: 1051 self._create_client_for_kernel(connection_file, hostname, sshkey, 1052 password) 1053 1054 def connect_client_to_kernel(self, client): 1055 """Connect a client to its kernel""" 1056 connection_file = client.connection_file 1057 1058 if self.test_no_stderr: 1059 stderr_file = None 1060 else: 1061 stderr_file = client.stderr_file 1062 1063 km, kc = self.create_kernel_manager_and_kernel_client(connection_file, 1064 stderr_file) 1065 # An error occurred if this is True 1066 if is_string(km) and kc is None: 1067 client.shellwidget.kernel_manager = None 1068 client.show_kernel_error(km) 1069 return 1070 1071 kc.started_channels.connect(lambda c=client: self.process_started(c)) 1072 kc.stopped_channels.connect(lambda c=client: self.process_finished(c)) 1073 kc.start_channels(shell=True, iopub=True) 1074 1075 shellwidget = client.shellwidget 1076 shellwidget.kernel_manager = km 1077 shellwidget.kernel_client = kc 1078 1079 def set_editor(self): 1080 """Set the editor used by the %edit magic""" 1081 # Get Python executable used by Spyder 1082 python = sys.executable 1083 if PY2: 1084 python = encoding.to_unicode_from_fs(python) 1085 1086 # Compose command for %edit 1087 spy_dir = osp.dirname(get_module_path('spyder')) 1088 if DEV: 1089 bootstrap = osp.join(spy_dir, 'bootstrap.py') 1090 if PY2: 1091 bootstrap = encoding.to_unicode_from_fs(bootstrap) 1092 editor = u'"{0}" "{1}" --'.format(python, bootstrap) 1093 else: 1094 import1 = "import sys" 1095 # We need to add spy_dir to sys.path so this test can be 1096 # run in our CIs 1097 if PYTEST: 1098 if os.name == 'nt': 1099 import1 = (import1 + 1100 '; sys.path.append(""{}"")'.format(spy_dir)) 1101 else: 1102 import1 = (import1 + 1103 "; sys.path.append('{}')".format(spy_dir)) 1104 import2 = "from spyder.app.start import send_args_to_spyder" 1105 code = "send_args_to_spyder([sys.argv[-1]])" 1106 editor = u"\"{0}\" -c \"{1}; {2}; {3}\"".format(python, 1107 import1, 1108 import2, 1109 code) 1110 1111 return editor 1112 1113 def config_options(self): 1114 """ 1115 Generate a Trailets Config instance for shell widgets using our 1116 config system 1117 1118 This lets us create each widget with its own config 1119 """ 1120 # ---- Jupyter config ---- 1121 try: 1122 full_cfg = load_pyconfig_files(['jupyter_qtconsole_config.py'], 1123 jupyter_config_dir()) 1124 1125 # From the full config we only select the JupyterWidget section 1126 # because the others have no effect here. 1127 cfg = Config({'JupyterWidget': full_cfg.JupyterWidget}) 1128 except: 1129 cfg = Config() 1130 1131 # ---- Spyder config ---- 1132 spy_cfg = Config() 1133 1134 # Make the pager widget a rich one (i.e a QTextEdit) 1135 spy_cfg.JupyterWidget.kind = 'rich' 1136 1137 # Gui completion widget 1138 completion_type_o = self.get_option('completion_type') 1139 completions = {0: "droplist", 1: "ncurses", 2: "plain"} 1140 spy_cfg.JupyterWidget.gui_completion = completions[completion_type_o] 1141 1142 # Pager 1143 pager_o = self.get_option('use_pager') 1144 if pager_o: 1145 spy_cfg.JupyterWidget.paging = 'inside' 1146 else: 1147 spy_cfg.JupyterWidget.paging = 'none' 1148 1149 # Calltips 1150 calltips_o = self.get_option('show_calltips') 1151 spy_cfg.JupyterWidget.enable_calltips = calltips_o 1152 1153 # Buffer size 1154 buffer_size_o = self.get_option('buffer_size') 1155 spy_cfg.JupyterWidget.buffer_size = buffer_size_o 1156 1157 # Prompts 1158 in_prompt_o = self.get_option('in_prompt') 1159 out_prompt_o = self.get_option('out_prompt') 1160 if in_prompt_o: 1161 spy_cfg.JupyterWidget.in_prompt = in_prompt_o 1162 if out_prompt_o: 1163 spy_cfg.JupyterWidget.out_prompt = out_prompt_o 1164 1165 # Style 1166 color_scheme = CONF.get('color_schemes', 'selected') 1167 style_sheet = create_qss_style(color_scheme)[0] 1168 spy_cfg.JupyterWidget.style_sheet = style_sheet 1169 spy_cfg.JupyterWidget.syntax_style = color_scheme 1170 1171 # Editor for %edit 1172 if CONF.get('main', 'single_instance'): 1173 spy_cfg.JupyterWidget.editor = self.set_editor() 1174 1175 # Merge QtConsole and Spyder configs. Spyder prefs will have 1176 # prevalence over QtConsole ones 1177 cfg._merge(spy_cfg) 1178 return cfg 1179 1180 def interpreter_versions(self): 1181 """Python and IPython versions used by clients""" 1182 if CONF.get('main_interpreter', 'default'): 1183 from IPython.core import release 1184 versions = dict( 1185 python_version = sys.version.split("\n")[0].strip(), 1186 ipython_version = release.version 1187 ) 1188 else: 1189 import subprocess 1190 versions = {} 1191 pyexec = CONF.get('main_interpreter', 'executable') 1192 py_cmd = "%s -c 'import sys; print(sys.version.split(\"\\n\")[0])'" % \ 1193 pyexec 1194 ipy_cmd = "%s -c 'import IPython.core.release as r; print(r.version)'" \ 1195 % pyexec 1196 for cmd in [py_cmd, ipy_cmd]: 1197 try: 1198 proc = programs.run_shell_command(cmd) 1199 output, _err = proc.communicate() 1200 except subprocess.CalledProcessError: 1201 output = '' 1202 output = output.decode().split('\n')[0].strip() 1203 if 'IPython' in cmd: 1204 versions['ipython_version'] = output 1205 else: 1206 versions['python_version'] = output 1207 1208 return versions 1209 1210 def additional_options(self): 1211 """ 1212 Additional options for shell widgets that are not defined 1213 in JupyterWidget config options 1214 """ 1215 options = dict( 1216 pylab=self.get_option('pylab'), 1217 autoload_pylab=self.get_option('pylab/autoload'), 1218 sympy=self.get_option('symbolic_math'), 1219 show_banner=self.get_option('show_banner') 1220 ) 1221 1222 return options 1223 1224 def register_client(self, client, give_focus=True): 1225 """Register new client""" 1226 client.configure_shellwidget(give_focus=give_focus) 1227 1228 # Local vars 1229 shellwidget = client.shellwidget 1230 control = shellwidget._control 1231 page_control = shellwidget._page_control 1232 1233 # Create new clients with Ctrl+T shortcut 1234 shellwidget.new_client.connect(self.create_new_client) 1235 1236 # For tracebacks 1237 control.go_to_error.connect(self.go_to_error) 1238 1239 shellwidget.sig_pdb_step.connect( 1240 lambda fname, lineno, shellwidget=shellwidget: 1241 self.pdb_has_stopped(fname, lineno, shellwidget)) 1242 1243 # Set shell cwd according to preferences 1244 cwd_path = '' 1245 if CONF.get('workingdir', 'console/use_project_or_home_directory'): 1246 cwd_path = get_home_dir() 1247 if (self.projects is not None and 1248 self.projects.get_active_project() is not None): 1249 cwd_path = self.projects.get_active_project_path() 1250 elif CONF.get('workingdir', 'console/use_fixed_directory'): 1251 cwd_path = CONF.get('workingdir', 'console/fixed_directory') 1252 1253 if osp.isdir(cwd_path) and self.main is not None: 1254 shellwidget.set_cwd(cwd_path) 1255 if give_focus: 1256 # Syncronice cwd with explorer and cwd widget 1257 shellwidget.get_cwd() 1258 1259 # Connect text widget to Help 1260 if self.help is not None: 1261 control.set_help(self.help) 1262 control.set_help_enabled(CONF.get('help', 'connect/ipython_console')) 1263 1264 # Connect client to our history log 1265 if self.historylog is not None: 1266 self.historylog.add_history(client.history_filename) 1267 client.append_to_history.connect(self.historylog.append_to_history) 1268 1269 # Set font for client 1270 client.set_font( self.get_plugin_font() ) 1271 1272 # Connect focus signal to client's control widget 1273 control.focus_changed.connect(lambda: self.focus_changed.emit()) 1274 1275 shellwidget.sig_change_cwd.connect(self.set_working_directory) 1276 1277 # Update the find widget if focus changes between control and 1278 # page_control 1279 self.find_widget.set_editor(control) 1280 if page_control: 1281 page_control.focus_changed.connect(lambda: self.focus_changed.emit()) 1282 control.visibility_changed.connect(self.refresh_plugin) 1283 page_control.visibility_changed.connect(self.refresh_plugin) 1284 page_control.show_find_widget.connect(self.find_widget.show) 1285 1286 def close_client(self, index=None, client=None, force=False): 1287 """Close client tab from index or widget (or close current tab)""" 1288 if not self.tabwidget.count(): 1289 return 1290 if client is not None: 1291 index = self.tabwidget.indexOf(client) 1292 if index is None and client is None: 1293 index = self.tabwidget.currentIndex() 1294 if index is not None: 1295 client = self.tabwidget.widget(index) 1296 1297 # Needed to handle a RuntimeError. See issue 5568. 1298 try: 1299 # Close client 1300 client.stop_button_click_handler() 1301 except RuntimeError: 1302 pass 1303 1304 # Disconnect timer needed to update elapsed time 1305 try: 1306 client.timer.timeout.disconnect(client.show_time) 1307 except (RuntimeError, TypeError): 1308 pass 1309 1310 # Check if related clients or kernels are opened 1311 # and eventually ask before closing them 1312 if not self.mainwindow_close and not force: 1313 close_all = True 1314 if self.get_option('ask_before_closing'): 1315 close = QMessageBox.question(self, self.get_plugin_title(), 1316 _("Do you want to close this console?"), 1317 QMessageBox.Yes | QMessageBox.No) 1318 if close == QMessageBox.No: 1319 return 1320 if len(self.get_related_clients(client)) > 0: 1321 close_all = QMessageBox.question(self, self.get_plugin_title(), 1322 _("Do you want to close all other consoles connected " 1323 "to the same kernel as this one?"), 1324 QMessageBox.Yes | QMessageBox.No) 1325 client.shutdown() 1326 if close_all == QMessageBox.Yes: 1327 self.close_related_clients(client) 1328 client.close() 1329 1330 # Note: client index may have changed after closing related widgets 1331 self.tabwidget.removeTab(self.tabwidget.indexOf(client)) 1332 self.clients.remove(client) 1333 1334 # This is needed to prevent that hanged consoles make reference 1335 # to an index that doesn't exist. See issue 4881 1336 try: 1337 self.filenames.pop(index) 1338 except IndexError: 1339 pass 1340 1341 self.update_tabs_text() 1342 1343 # Create a new client if the console is about to become empty 1344 if not self.tabwidget.count() and self.create_new_client_if_empty: 1345 self.create_new_client() 1346 1347 self.update_plugin_title.emit() 1348 1349 def get_client_index_from_id(self, client_id): 1350 """Return client index from id""" 1351 for index, client in enumerate(self.clients): 1352 if id(client) == client_id: 1353 return index 1354 1355 def get_related_clients(self, client): 1356 """ 1357 Get all other clients that are connected to the same kernel as `client` 1358 """ 1359 related_clients = [] 1360 for cl in self.get_clients(): 1361 if cl.connection_file == client.connection_file and \ 1362 cl is not client: 1363 related_clients.append(cl) 1364 return related_clients 1365 1366 def close_related_clients(self, client): 1367 """Close all clients related to *client*, except itself""" 1368 related_clients = self.get_related_clients(client) 1369 for cl in related_clients: 1370 self.close_client(client=cl, force=True) 1371 1372 def restart(self): 1373 """ 1374 Restart the console 1375 1376 This is needed when we switch projects to update PYTHONPATH 1377 and the selected interpreter 1378 """ 1379 self.master_clients = 0 1380 self.create_new_client_if_empty = False 1381 for i in range(len(self.clients)): 1382 client = self.clients[-1] 1383 try: 1384 client.shutdown() 1385 except Exception as e: 1386 QMessageBox.warning(self, _('Warning'), 1387 _("It was not possible to restart the IPython console " 1388 "when switching to this project. " 1389 "The error was {0}").format(e), QMessageBox.Ok) 1390 self.close_client(client=client, force=True) 1391 self.create_new_client(give_focus=False) 1392 self.create_new_client_if_empty = True 1393 1394 def pdb_has_stopped(self, fname, lineno, shellwidget): 1395 """Python debugger has just stopped at frame (fname, lineno)""" 1396 # This is a unique form of the edit_goto signal that is intended to 1397 # prevent keyboard input from accidentally entering the editor 1398 # during repeated, rapid entry of debugging commands. 1399 self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False) 1400 self.activateWindow() 1401 shellwidget._control.setFocus() 1402 1403 def set_spyder_breakpoints(self): 1404 """Set Spyder breakpoints into all clients""" 1405 for cl in self.clients: 1406 cl.shellwidget.set_spyder_breakpoints() 1407 1408 @Slot(str) 1409 def create_client_from_path(self, path): 1410 """Create a client with its cwd pointing to path.""" 1411 self.create_new_client() 1412 sw = self.get_current_shellwidget() 1413 sw.set_cwd(path) 1414 1415 def create_client_for_file(self, filename): 1416 """Create a client to execute code related to a file.""" 1417 # Create client 1418 self.create_new_client(filename=filename) 1419 1420 # Don't increase the count of master clients 1421 self.master_clients -= 1 1422 1423 # Rename client tab with filename 1424 client = self.get_current_client() 1425 client.allow_rename = False 1426 tab_text = self.disambiguate_fname(filename) 1427 self.rename_client_tab(client, tab_text) 1428 1429 def get_client_for_file(self, filename): 1430 """Get client associated with a given file.""" 1431 client = None 1432 for idx, cl in enumerate(self.get_clients()): 1433 if self.filenames[idx] == filename: 1434 self.tabwidget.setCurrentIndex(idx) 1435 client = cl 1436 break 1437 return client 1438 1439 def set_elapsed_time(self, client): 1440 """Set elapsed time for slave clients.""" 1441 related_clients = self.get_related_clients(client) 1442 for cl in related_clients: 1443 if cl.timer is not None: 1444 client.create_time_label() 1445 client.t0 = cl.t0 1446 client.timer.timeout.connect(client.show_time) 1447 client.timer.start(1000) 1448 break 1449 1450 #------ Public API (for kernels) ------------------------------------------ 1451 def ssh_tunnel(self, *args, **kwargs): 1452 if os.name == 'nt': 1453 return zmqtunnel.paramiko_tunnel(*args, **kwargs) 1454 else: 1455 return openssh_tunnel(self, *args, **kwargs) 1456 1457 def tunnel_to_kernel(self, connection_info, hostname, sshkey=None, 1458 password=None, timeout=10): 1459 """ 1460 Tunnel connections to a kernel via ssh. 1461 1462 Remote ports are specified in the connection info ci. 1463 """ 1464 lports = zmqtunnel.select_random_ports(4) 1465 rports = (connection_info['shell_port'], connection_info['iopub_port'], 1466 connection_info['stdin_port'], connection_info['hb_port']) 1467 remote_ip = connection_info['ip'] 1468 for lp, rp in zip(lports, rports): 1469 self.ssh_tunnel(lp, rp, hostname, remote_ip, sshkey, password, 1470 timeout) 1471 return tuple(lports) 1472 1473 def create_kernel_spec(self): 1474 """Create a kernel spec for our own kernels""" 1475 # Before creating our kernel spec, we always need to 1476 # set this value in spyder.ini 1477 if not self.testing: 1478 CONF.set('main', 'spyder_pythonpath', 1479 self.main.get_spyder_pythonpath()) 1480 return SpyderKernelSpec() 1481 1482 def create_kernel_manager_and_kernel_client(self, connection_file, 1483 stderr_file): 1484 """Create kernel manager and client.""" 1485 # Kernel spec 1486 kernel_spec = self.create_kernel_spec() 1487 if not kernel_spec.env.get('PYTHONPATH'): 1488 error_msg = _("This error was most probably caused by installing " 1489 "Spyder in a directory with non-ascii characters " 1490 "(i.e. characters with tildes, apostrophes or " 1491 "non-latin symbols).<br><br>" 1492 "To fix it, please <b>reinstall</b> Spyder in a " 1493 "different location.") 1494 return (error_msg, None) 1495 1496 # Kernel manager 1497 kernel_manager = QtKernelManager(connection_file=connection_file, 1498 config=None, autorestart=True) 1499 kernel_manager._kernel_spec = kernel_spec 1500 1501 # Save stderr in a file to read it later in case of errors 1502 if stderr_file is not None: 1503 # Needed to prevent any error that could appear. 1504 # See issue 6267 1505 try: 1506 stderr = codecs.open(stderr_file, 'w', encoding='utf-8') 1507 except Exception: 1508 stderr = None 1509 else: 1510 stderr = None 1511 kernel_manager.start_kernel(stderr=stderr) 1512 1513 # Kernel client 1514 kernel_client = kernel_manager.client() 1515 1516 # Increase time to detect if a kernel is alive 1517 # See Issue 3444 1518 kernel_client.hb_channel.time_to_dead = 18.0 1519 1520 return kernel_manager, kernel_client 1521 1522 def restart_kernel(self): 1523 """Restart kernel of current client.""" 1524 client = self.get_current_client() 1525 if client is not None: 1526 client.restart_kernel() 1527 1528 #------ Public API (for tabs) --------------------------------------------- 1529 def add_tab(self, widget, name, filename=''): 1530 """Add tab""" 1531 self.clients.append(widget) 1532 index = self.tabwidget.addTab(widget, name) 1533 self.filenames.insert(index, filename) 1534 self.tabwidget.setCurrentIndex(index) 1535 if self.dockwidget and not self.ismaximized: 1536 self.dockwidget.setVisible(True) 1537 self.dockwidget.raise_() 1538 self.activateWindow() 1539 widget.get_control().setFocus() 1540 self.update_tabs_text() 1541 1542 def move_tab(self, index_from, index_to): 1543 """ 1544 Move tab (tabs themselves have already been moved by the tabwidget) 1545 """ 1546 filename = self.filenames.pop(index_from) 1547 client = self.clients.pop(index_from) 1548 self.filenames.insert(index_to, filename) 1549 self.clients.insert(index_to, client) 1550 self.update_tabs_text() 1551 self.update_plugin_title.emit() 1552 1553 def disambiguate_fname(self, fname): 1554 """Generate a file name without ambiguation.""" 1555 files_path_list = [filename for filename in self.filenames 1556 if filename] 1557 return sourcecode.disambiguate_fname(files_path_list, fname) 1558 1559 def update_tabs_text(self): 1560 """Update the text from the tabs.""" 1561 # This is needed to prevent that hanged consoles make reference 1562 # to an index that doesn't exist. See issue 4881 1563 try: 1564 for index, fname in enumerate(self.filenames): 1565 client = self.clients[index] 1566 if fname: 1567 self.rename_client_tab(client, 1568 self.disambiguate_fname(fname)) 1569 else: 1570 self.rename_client_tab(client, None) 1571 except IndexError: 1572 pass 1573 1574 def rename_client_tab(self, client, given_name): 1575 """Rename client's tab""" 1576 index = self.get_client_index_from_id(id(client)) 1577 1578 if given_name is not None: 1579 client.given_name = given_name 1580 self.tabwidget.setTabText(index, client.get_name()) 1581 1582 def rename_tabs_after_change(self, given_name): 1583 """Rename tabs after a change in name.""" 1584 client = self.get_current_client() 1585 1586 # Prevent renames that want to assign the same name of 1587 # a previous tab 1588 repeated = False 1589 for cl in self.get_clients(): 1590 if id(client) != id(cl) and given_name == cl.given_name: 1591 repeated = True 1592 break 1593 1594 # Rename current client tab to add str_id 1595 if client.allow_rename and not u'/' in given_name and not repeated: 1596 self.rename_client_tab(client, given_name) 1597 else: 1598 self.rename_client_tab(client, None) 1599 1600 # Rename related clients 1601 if client.allow_rename and not u'/' in given_name and not repeated: 1602 for cl in self.get_related_clients(client): 1603 self.rename_client_tab(cl, given_name) 1604 1605 def tab_name_editor(self): 1606 """Trigger the tab name editor.""" 1607 index = self.tabwidget.currentIndex() 1608 self.tabwidget.tabBar().tab_name_editor.edit_tab(index) 1609 1610 #------ Public API (for help) --------------------------------------------- 1611 def go_to_error(self, text): 1612 """Go to error if relevant""" 1613 match = get_error_match(to_text_string(text)) 1614 if match: 1615 fname, lnb = match.groups() 1616 self.edit_goto.emit(osp.abspath(fname), int(lnb), '') 1617 1618 @Slot() 1619 def show_intro(self): 1620 """Show intro to IPython help""" 1621 from IPython.core.usage import interactive_usage 1622 self.help.show_rich_text(interactive_usage) 1623 1624 @Slot() 1625 def show_guiref(self): 1626 """Show qtconsole help""" 1627 from qtconsole.usage import gui_reference 1628 self.help.show_rich_text(gui_reference, collapse=True) 1629 1630 @Slot() 1631 def show_quickref(self): 1632 """Show IPython Cheat Sheet""" 1633 from IPython.core.usage import quick_reference 1634 self.help.show_plain_text(quick_reference) 1635 1636 #------ Private API ------------------------------------------------------- 1637 def _new_connection_file(self): 1638 """ 1639 Generate a new connection file 1640 1641 Taken from jupyter_client/console_app.py 1642 Licensed under the BSD license 1643 """ 1644 # Check if jupyter_runtime_dir exists (Spyder addition) 1645 if not osp.isdir(jupyter_runtime_dir()): 1646 try: 1647 os.makedirs(jupyter_runtime_dir()) 1648 except (IOError, OSError): 1649 return None 1650 cf = '' 1651 while not cf: 1652 ident = str(uuid.uuid4()).split('-')[-1] 1653 cf = os.path.join(jupyter_runtime_dir(), 'kernel-%s.json' % ident) 1654 cf = cf if not os.path.exists(cf) else '' 1655 return cf 1656 1657 def process_started(self, client): 1658 if self.help is not None: 1659 self.help.set_shell(client.shellwidget) 1660 if self.variableexplorer is not None: 1661 self.variableexplorer.add_shellwidget(client.shellwidget) 1662 1663 def process_finished(self, client): 1664 if self.variableexplorer is not None: 1665 self.variableexplorer.remove_shellwidget(id(client.shellwidget)) 1666 1667 def connect_external_kernel(self, shellwidget): 1668 """ 1669 Connect an external kernel to the Variable Explorer and Help, if 1670 it is a Spyder kernel. 1671 """ 1672 sw = shellwidget 1673 kc = shellwidget.kernel_client 1674 if self.help is not None: 1675 self.help.set_shell(sw) 1676 if self.variableexplorer is not None: 1677 self.variableexplorer.add_shellwidget(sw) 1678 sw.set_namespace_view_settings() 1679 sw.refresh_namespacebrowser() 1680 kc.stopped_channels.connect(lambda : 1681 self.variableexplorer.remove_shellwidget(id(sw))) 1682 1683 def _create_client_for_kernel(self, connection_file, hostname, sshkey, 1684 password): 1685 # Verifying if the connection file exists 1686 try: 1687 cf_path = osp.dirname(connection_file) 1688 cf_filename = osp.basename(connection_file) 1689 # To change a possible empty string to None 1690 cf_path = cf_path if cf_path else None 1691 connection_file = find_connection_file(filename=cf_filename, 1692 path=cf_path) 1693 except (IOError, UnboundLocalError): 1694 QMessageBox.critical(self, _('IPython'), 1695 _("Unable to connect to " 1696 "<b>%s</b>") % connection_file) 1697 return 1698 1699 # Getting the master id that corresponds to the client 1700 # (i.e. the i in i/A) 1701 master_id = None 1702 given_name = None 1703 external_kernel = False 1704 slave_ord = ord('A') - 1 1705 kernel_manager = None 1706 1707 for cl in self.get_clients(): 1708 if connection_file in cl.connection_file: 1709 if cl.get_kernel() is not None: 1710 kernel_manager = cl.get_kernel() 1711 connection_file = cl.connection_file 1712 if master_id is None: 1713 master_id = cl.id_['int_id'] 1714 given_name = cl.given_name 1715 new_slave_ord = ord(cl.id_['str_id']) 1716 if new_slave_ord > slave_ord: 1717 slave_ord = new_slave_ord 1718 1719 # If we couldn't find a client with the same connection file, 1720 # it means this is a new master client 1721 if master_id is None: 1722 self.master_clients += 1 1723 master_id = to_text_string(self.master_clients) 1724 external_kernel = True 1725 1726 # Set full client name 1727 client_id = dict(int_id=master_id, 1728 str_id=chr(slave_ord + 1)) 1729 1730 # Creating the client 1731 show_elapsed_time = self.get_option('show_elapsed_time') 1732 reset_warning = self.get_option('show_reset_namespace_warning') 1733 client = ClientWidget(self, 1734 id_=client_id, 1735 given_name=given_name, 1736 history_filename=get_conf_path('history.py'), 1737 config_options=self.config_options(), 1738 additional_options=self.additional_options(), 1739 interpreter_versions=self.interpreter_versions(), 1740 connection_file=connection_file, 1741 menu_actions=self.menu_actions, 1742 hostname=hostname, 1743 external_kernel=external_kernel, 1744 slave=True, 1745 show_elapsed_time=show_elapsed_time, 1746 reset_warning=reset_warning) 1747 1748 # Create kernel client 1749 kernel_client = QtKernelClient(connection_file=connection_file) 1750 kernel_client.load_connection_file() 1751 if hostname is not None: 1752 try: 1753 connection_info = dict(ip = kernel_client.ip, 1754 shell_port = kernel_client.shell_port, 1755 iopub_port = kernel_client.iopub_port, 1756 stdin_port = kernel_client.stdin_port, 1757 hb_port = kernel_client.hb_port) 1758 newports = self.tunnel_to_kernel(connection_info, hostname, 1759 sshkey, password) 1760 (kernel_client.shell_port, 1761 kernel_client.iopub_port, 1762 kernel_client.stdin_port, 1763 kernel_client.hb_port) = newports 1764 except Exception as e: 1765 QMessageBox.critical(self, _('Connection error'), 1766 _("Could not open ssh tunnel. The " 1767 "error was:\n\n") + to_text_string(e)) 1768 return 1769 1770 # Assign kernel manager and client to shellwidget 1771 client.shellwidget.kernel_client = kernel_client 1772 client.shellwidget.kernel_manager = kernel_manager 1773 kernel_client.start_channels() 1774 if external_kernel: 1775 client.shellwidget.sig_is_spykernel.connect( 1776 self.connect_external_kernel) 1777 client.shellwidget.is_spyder_kernel() 1778 1779 # Set elapsed time, if possible 1780 if not external_kernel: 1781 self.set_elapsed_time(client) 1782 1783 # Adding a new tab for the client 1784 self.add_tab(client, name=client.get_name()) 1785 1786 # Register client 1787 self.register_client(client) 1788