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].&lt;Tab&gt;</tt> or "
488                                "<tt>ins.meth().&lt;Tab&gt;</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 [&lt;span class="in-prompt-number"&gt;'
551                                    '%i&lt;/span&gt;]:'),
552                                    alignment=Qt.Horizontal)
553        out_prompt_edit = self.create_lineedit(_("Output prompt:"),
554                                   'out_prompt', '',
555                                 _('Default is<br>'
556                                   'Out[&lt;span class="out-prompt-number"&gt;'
557                                   '%i&lt;/span&gt;]:'),
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