[spyder] 01/03: New upstream version 3.1.3+dfsg1

Ghislain Vaillant ghisvail-guest at moszumanska.debian.org
Tue Feb 21 12:25:22 UTC 2017


This is an automated email from the git hooks/post-receive script.

ghisvail-guest pushed a commit to branch experimental
in repository spyder.

commit f3a1c4db7a52d3d9b29a267e969146f940c32284
Author: Ghislain Antony Vaillant <ghisvail at gmail.com>
Date:   Tue Feb 21 12:08:33 2017 +0000

    New upstream version 3.1.3+dfsg1
---
 PKG-INFO                                           |   4 +-
 README.md                                          |   2 +-
 doc/installation.rst                               |   2 +-
 setup.py                                           |   5 +-
 spyder/__init__.py                                 |   2 +-
 spyder/app/mainwindow.py                           |  65 +++--
 spyder/config/base.py                              |   6 +
 spyder/plugins/editor.py                           |  41 +--
 spyder/plugins/externalconsole.py                  |  18 +-
 spyder/plugins/findinfiles.py                      |   1 +
 spyder/plugins/ipythonconsole.py                   |  37 ++-
 spyder/plugins/tests/test_ipythonconsole.py        | 129 +++++++++-
 spyder/utils/fixtures.py                           |  26 ++
 spyder/utils/introspection/docstrings.py           | 285 ---------------------
 spyder/utils/introspection/jedi_patch.py           |  13 +-
 spyder/utils/introspection/manager.py              |   2 +-
 spyder/utils/introspection/numpy_docstr.py         | 150 +++++++++++
 .../utils/introspection/test/test_jedi_plugin.py   |   2 +-
 spyder/utils/iofuncs.py                            |  15 +-
 spyder/utils/ipython/spyder_kernel.py              |   8 +-
 spyder/widgets/editor.py                           |   4 +-
 spyder/widgets/findinfiles.py                      |   9 +-
 spyder/widgets/ipythonconsole/debugging.py         |   6 +
 spyder/widgets/ipythonconsole/namespacebrowser.py  |   4 +-
 spyder/widgets/mixins.py                           |   2 +
 spyder/widgets/sourcecode/base.py                  |   2 +
 spyder/widgets/sourcecode/codeeditor.py            |  63 ++++-
 .../widgets/variableexplorer/collectionseditor.py  |   8 +-
 spyder/widgets/variableexplorer/dataframeeditor.py |  12 +-
 29 files changed, 522 insertions(+), 401 deletions(-)

diff --git a/PKG-INFO b/PKG-INFO
index d5ecbc5..c825a0e 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,12 +1,12 @@
 Metadata-Version: 1.1
 Name: spyder
-Version: 3.1.2
+Version: 3.1.3
 Summary: Scientific PYthon Development EnviRonment
 Home-page: https://github.com/spyder-ide/spyder
 Author: The Spyder Project Contributors
 Author-email: UNKNOWN
 License: MIT
-Download-URL: https://github.com/spyder-ide/spyder/files/spyder-3.1.2.zip
+Download-URL: https://github.com/spyder-ide/spyder/files/spyder-3.1.3.zip
 Description: Spyder is an interactive Python development environment providing
         MATLAB-like features in a simple and light-weighted software.
         It also provides ready-to-use pure-Python widgets to your PyQt5 or
diff --git a/README.md b/README.md
index 2e110bd..4011720 100644
--- a/README.md
+++ b/README.md
@@ -143,7 +143,7 @@ a Python version greater than 2.7 (Python 3.2 is not supported anymore).
 * **Python** 2.7 or 3.3+
 * **PyQt5** 5.2+ or **PyQt4** 4.6+: PyQt5 is recommended.
 * **qtconsole** 4.2.0+: Enhanced Python interpreter.
-* **Rope** and **Jedi**: Editor code completion, calltips
+* **Rope** and **Jedi** 0.9.0: Editor code completion, calltips
   and go-to-definition.
 * **Pyflakes**: Real-time code analysis.
 * **Sphinx**: Rich text mode for the Help pane.
diff --git a/doc/installation.rst b/doc/installation.rst
index 47abb53..678ba8f 100644
--- a/doc/installation.rst
+++ b/doc/installation.rst
@@ -161,7 +161,7 @@ The requirements to run Spyder are:
   enhanced Python interpreter.
 
 * `Rope <http://rope.sourceforge.net/>`_ >=0.9.4 and
-  `Jedi <http://jedi.jedidjah.ch/en/latest/>` 0.8.1 -- for code completion,
+  `Jedi <http://jedi.jedidjah.ch/en/latest/>` 0.9.0 -- for code completion,
   go-to-definition and calltips on the Editor.
 
 * `Pyflakes <http://pypi.python.org/pypi/pyflakes>`_  -- for real-time
diff --git a/setup.py b/setup.py
index 0bcbc90..3ec0af3 100644
--- a/setup.py
+++ b/setup.py
@@ -271,7 +271,7 @@ if any(arg == 'bdist_wheel' for arg in sys.argv):
 
 install_requires = [
     'rope_py3k' if PY3 else 'rope>=0.9.4',
-    'jedi',
+    'jedi==0.9.0',
     'pyflakes',
     'pygments>=2.0',
     'qtconsole>=4.2.0',
@@ -293,7 +293,8 @@ if 'setuptools' in sys.modules:
 
     setup_args['entry_points'] = {
         'gui_scripts': [
-            'spyder = spyder.app.start:main'
+            '{} = spyder.app.start:main'.format(
+                'spyder3' if PY3 else 'spyder')
         ]
     }
 
diff --git a/spyder/__init__.py b/spyder/__init__.py
index 990a93d..14bad73 100644
--- a/spyder/__init__.py
+++ b/spyder/__init__.py
@@ -27,7 +27,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 OTHER DEALINGS IN THE SOFTWARE.
 """
 
-version_info = (3, 1, 2)
+version_info = (3, 1, 3)
 
 __version__ = '.'.join(map(str, version_info))
 __license__ = __doc__
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
index 23761a1..bb3d40f 100644
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -119,15 +119,18 @@ MAIN_APP = qapplication()
 #==============================================================================
 # Create splash screen out of MainWindow to reduce perceived startup time. 
 #==============================================================================
-from spyder.config.base import _, get_image_path, DEV
-SPLASH = QSplashScreen(QPixmap(get_image_path('splash.svg'), 'svg'))
-SPLASH_FONT = SPLASH.font()
-SPLASH_FONT.setPixelSize(10)
-SPLASH.setFont(SPLASH_FONT)
-SPLASH.show()
-SPLASH.showMessage(_("Initializing..."), Qt.AlignBottom | Qt.AlignCenter |
-                   Qt.AlignAbsolute, QColor(Qt.white))
-QApplication.processEvents()
+from spyder.config.base import _, get_image_path, DEV, PYTEST
+if not PYTEST:
+    SPLASH = QSplashScreen(QPixmap(get_image_path('splash.svg')))
+    SPLASH_FONT = SPLASH.font()
+    SPLASH_FONT.setPixelSize(10)
+    SPLASH.setFont(SPLASH_FONT)
+    SPLASH.show()
+    SPLASH.showMessage(_("Initializing..."), Qt.AlignBottom | Qt.AlignCenter |
+                    Qt.AlignAbsolute, QColor(Qt.white))
+    QApplication.processEvents()
+else:
+    SPLASH = None
 
 
 #==============================================================================
@@ -264,8 +267,6 @@ class MainWindow(QMainWindow):
         self.open_project = options.open_project
 
         self.debug_print("Start of MainWindow constructor")
-        
-        self.setFocusPolicy(Qt.StrongFocus)
 
         def signal_handler(signum, frame=None):
             """Handler for signals."""
@@ -1120,7 +1121,8 @@ class MainWindow(QMainWindow):
                 menu_object.aboutToHide.connect(
                     lambda name=name: self.hide_shortcuts(name))
 
-        self.splash.hide()
+        if self.splash is not None:
+            self.splash.hide()
 
         # Enabling tear off for all menus except help menu
         if CONF.get('main', 'tear_off_menus'):
@@ -1131,7 +1133,10 @@ class MainWindow(QMainWindow):
         # Menu about to show
         for child in self.menuBar().children():
             if isinstance(child, QMenu):
-                child.aboutToShow.connect(self.update_edit_menu)
+                try:
+                    child.aboutToShow.connect(self.update_edit_menu)
+                except TypeError:
+                    pass
 
         self.debug_print("*** End of MainWindow setup ***")
         self.is_starting_up = False
@@ -1945,17 +1950,20 @@ class MainWindow(QMainWindow):
         """Get properties of focus widget
         Returns tuple (widget, properties) where properties is a tuple of
         booleans: (is_console, not_readonly, readwrite_editor)"""
-        widget = self.focusWidget()
+        widget = QApplication.focusWidget()
         from spyder.widgets.shell import ShellBaseWidget
         from spyder.widgets.editor import TextEditBaseWidget
+        from spyder.widgets.ipythonconsole import ControlWidget
 
         # if focused widget isn't valid try the last focused
-        if not isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)):
+        if not isinstance(widget, (ShellBaseWidget, TextEditBaseWidget,
+                                   ControlWidget)):
             widget = self.previous_focused_widget
 
         textedit_properties = None
-        if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget)):
-            console = isinstance(widget, ShellBaseWidget)
+        if isinstance(widget, (ShellBaseWidget, TextEditBaseWidget,
+                               ControlWidget)):
+            console = isinstance(widget, (ShellBaseWidget, ControlWidget))
             not_readonly = not widget.isReadOnly()
             readwrite_editor = not_readonly and not console
             textedit_properties = (console, not_readonly, readwrite_editor)
@@ -2004,7 +2012,10 @@ class MainWindow(QMainWindow):
 
         widget, textedit_properties = self.get_focus_widget_properties()
         for action in self.editor.search_menu_actions:
-            action.setEnabled(self.editor.isAncestorOf(widget))
+            try:
+                action.setEnabled(self.editor.isAncestorOf(widget))
+            except RuntimeError:
+                pass
         if textedit_properties is None: # widget is not an editor/console
             return
         #!!! Below this line, widget is expected to be a QPlainTextEdit instance
@@ -2063,6 +2074,8 @@ class MainWindow(QMainWindow):
 
     def set_splash(self, message):
         """Set splash message"""
+        if self.splash is None:
+            return
         if message:
             self.debug_print(message)
         self.splash.show()
@@ -2099,12 +2112,6 @@ class MainWindow(QMainWindow):
         # To be used by the tour to be able to move
         self.sig_moved.emit(event)
 
-    def focusInEvent(self, event):
-        """Reimplement Qt method."""
-        QMainWindow.focusInEvent(self, event)
-        if self.hasFocus():
-            self.tour.gain_focus()
-
     def hideEvent(self, event):
         """Reimplement Qt method"""
         for plugin in self.widgetlist:
@@ -2185,6 +2192,7 @@ class MainWindow(QMainWindow):
         self.maximize_action.setToolTip(tip)
 
     @Slot()
+    @Slot(bool)
     def maximize_dockwidget(self, restore=False):
         """Shortcut: Ctrl+Alt+Shift+M
         First call: maximize current dockwidget
@@ -2923,7 +2931,8 @@ def run_spyder(app, options, args):
     # the window
     app.focusChanged.connect(main.change_last_focused_widget)
 
-    app.exec_()
+    if not PYTEST:
+        app.exec_()
     return main
 
 
@@ -2964,7 +2973,8 @@ def main():
     # Show crash dialog
     if CONF.get('main', 'crash', False) and not DEV:
         CONF.set('main', 'crash', False)
-        SPLASH.hide()
+        if SPLASH is not None:
+            SPLASH.hide()
         QMessageBox.information(None, "Spyder",
             "Spyder crashed during last session.<br><br>"
             "If Spyder does not start at all and <u>before submitting a "
@@ -3001,7 +3011,8 @@ def main():
         traceback.print_exc(file=open('spyder_crash.log', 'w'))
     if mainwindow is None:
         # An exception occured
-        SPLASH.hide()
+        if SPLASH is not None:
+            SPLASH.hide()
         return
 
     ORIGINAL_SYS_EXIT()
diff --git a/spyder/config/base.py b/spyder/config/base.py
index 736dff4..c66a488 100644
--- a/spyder/config/base.py
+++ b/spyder/config/base.py
@@ -39,6 +39,11 @@ DEV = os.environ.get('SPYDER_DEV')
 TEST = os.environ.get('SPYDER_TEST')
 
 
+# To do some adjustments for pytest
+# This env var is defined in runtests.py
+PYTEST = os.environ.get('SPYDER_PYTEST')
+
+
 #==============================================================================
 # Debug helpers
 #==============================================================================
@@ -209,6 +214,7 @@ def get_image_path(name, default="not_found.png"):
         if osp.isfile(full_path):
             return osp.abspath(full_path)
     if default is not None:
+        img_path = osp.join(get_module_path('spyder'), 'images')
         return osp.abspath(osp.join(img_path, default))
 
 
diff --git a/spyder/plugins/editor.py b/spyder/plugins/editor.py
index e6d8670..25838b4 100644
--- a/spyder/plugins/editor.py
+++ b/spyder/plugins/editor.py
@@ -29,7 +29,7 @@ from qtpy.QtWidgets import (QAction, QActionGroup, QApplication, QDialog,
                             QToolBar, QVBoxLayout, QWidget)
 
 # Local imports
-from spyder.config.base import _, get_conf_path
+from spyder.config.base import _, get_conf_path, PYTEST
 from spyder.config.main import (CONF, RUN_CELL_SHORTCUT,
                                 RUN_CELL_AND_ADVANCE_SHORTCUT)
 from spyder.config.utils import (get_edit_filetypes, get_edit_filters,
@@ -542,19 +542,17 @@ class Editor(SpyderPluginWidget):
     def get_plugin_title(self):
         """Return widget title"""
         title = _('Editor')
-        filename = self.get_current_filename()
-        if filename:
-            title += ' - '+to_text_string(filename)
         return title
-    
+
     def get_plugin_icon(self):
-        """Return widget icon"""
+        """Return widget icon."""
         return ima.icon('edit')
     
     def get_focus_widget(self):
         """
-        Return the widget to give focus to when
-        this plugin's dockwidget is raised on top-level
+        Return the widget to give focus to.
+
+        This happens when plugin's dockwidget is raised on top-level.
         """
         return self.get_current_editor()
 
@@ -1477,9 +1475,10 @@ class Editor(SpyderPluginWidget):
     def refresh_save_all_action(self):
         """Enable 'Save All' if there are files to be saved"""
         editorstack = self.get_current_editorstack()
-        state = any(finfo.editor.document().isModified()
-                    for finfo in editorstack.data)
-        self.save_all_action.setEnabled(state)
+        if editorstack:
+            state = any(finfo.editor.document().isModified()
+                        for finfo in editorstack.data)
+            self.save_all_action.setEnabled(state)
             
     def update_warning_menu(self):
         """Update warning list menu"""
@@ -1814,11 +1813,19 @@ class Editor(SpyderPluginWidget):
                                             osp.splitext(filename0)[1])
             else:
                 selectedfilter = ''
-            filenames, _sf = getopenfilenames(parent_widget,
-                                     _("Open file"), basedir,
-                                     self.edit_filters,
-                                     selectedfilter=selectedfilter,
-                                     options=QFileDialog.HideNameFilterDetails)
+            if not PYTEST:
+                filenames, _sf = getopenfilenames(
+                                    parent_widget,
+                                    _("Open file"), basedir,
+                                    self.edit_filters,
+                                    selectedfilter=selectedfilter,
+                                    options=QFileDialog.HideNameFilterDetails)
+            else:
+                # Use a Qt (i.e. scriptable) dialog for pytest
+                dialog = QFileDialog(parent_widget, _("Open file"),
+                                     options=QFileDialog.DontUseNativeDialog)
+                if dialog.exec_():
+                    filenames = dialog.selectedFiles()
             self.redirect_stdio.emit(True)
             if filenames:
                 filenames = [osp.normpath(fname) for fname in filenames]
@@ -2312,7 +2319,7 @@ class Editor(SpyderPluginWidget):
                 if self.dialog_size is not None:
                     dialog.resize(self.dialog_size)
                 dialog.setup(fname)
-                if CONF.get('run', 'open_at_least_once', True):
+                if CONF.get('run', 'open_at_least_once', not PYTEST):
                     # Open Run Config dialog at least once: the first time 
                     # a script is ever run in Spyder, so that the user may 
                     # see it at least once and be conscious that it exists
diff --git a/spyder/plugins/externalconsole.py b/spyder/plugins/externalconsole.py
index c02b3f2..7ee49b1 100644
--- a/spyder/plugins/externalconsole.py
+++ b/spyder/plugins/externalconsole.py
@@ -515,11 +515,19 @@ class ExternalConsole(SpyderPluginWidget):
                 if old_shell.is_running():
                     runconfig = get_run_configuration(fname)
                     if runconfig is None or runconfig.show_kill_warning:
-                        answer = QMessageBox.question(self, self.get_plugin_title(),
-                            _("%s is already running in a separate process.\n"
-                              "Do you want to kill the process before starting "
-                              "a new one?") % osp.basename(fname),
-                            QMessageBox.Yes | QMessageBox.Cancel)
+                        if PYQT5:
+                            answer = QMessageBox.question(self, self.get_plugin_title(),
+                                _("%s is already running in a separate process.\n"
+                                  "Do you want to kill the process before starting "
+                                  "a new one?") % osp.basename(fname),
+                                QMessageBox.Yes | QMessageBox.Cancel)
+                        else:
+                            mb = QMessageBox(self)
+                            answer = mb.question(mb, self.get_plugin_title(),
+                                _("%s is already running in a separate process.\n"
+                                  "Do you want to kill the process before starting "
+                                  "a new one?") % osp.basename(fname),
+                                QMessageBox.Yes | QMessageBox.Cancel)
                     else:
                         answer = QMessageBox.Yes
 
diff --git a/spyder/plugins/findinfiles.py b/spyder/plugins/findinfiles.py
index 1d5dcb7..b6da05d 100644
--- a/spyder/plugins/findinfiles.py
+++ b/spyder/plugins/findinfiles.py
@@ -153,6 +153,7 @@ class FindInFiles(FindInFilesWidget, SpyderPluginMixin):
         self.main.search_menu_actions += [MENU_SEPARATOR, findinfiles_action]
         self.main.search_toolbar_actions += [MENU_SEPARATOR,
                                              findinfiles_action]
+        self.refreshdir()
     
     def refresh_plugin(self):
         """Refresh widget"""
diff --git a/spyder/plugins/ipythonconsole.py b/spyder/plugins/ipythonconsole.py
index 2b1d8cb..a0eb984 100644
--- a/spyder/plugins/ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole.py
@@ -206,12 +206,12 @@ class KernelConnectionDialog(QDialog):
         ssh_form.addRow(_('Password'), self.pw)
         
         # Ok and Cancel buttons
-        accept_btns = QDialogButtonBox(
+        self.accept_btns = QDialogButtonBox(
             QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
             Qt.Horizontal, self)
 
-        accept_btns.accepted.connect(self.accept)
-        accept_btns.rejected.connect(self.reject)
+        self.accept_btns.accepted.connect(self.accept)
+        self.accept_btns.rejected.connect(self.reject)
 
         # Dialog layout
         layout = QVBoxLayout(self)
@@ -219,7 +219,7 @@ class KernelConnectionDialog(QDialog):
         layout.addLayout(cf_layout)
         layout.addWidget(self.rm_cb)
         layout.addLayout(ssh_form)
-        layout.addWidget(accept_btns)
+        layout.addWidget(self.accept_btns)
                 
         # remote kernel checkbox enables the ssh_connection_form
         def ssh_set_enabled(state):
@@ -242,8 +242,9 @@ class KernelConnectionDialog(QDialog):
         self.kf.setText(kf)
 
     @staticmethod
-    def get_connection_parameters(parent=None):
-        dialog = KernelConnectionDialog(parent)
+    def get_connection_parameters(parent=None, dialog=None):
+        if not dialog:
+            dialog = KernelConnectionDialog(parent)
         result = dialog.exec_()
         is_remote = bool(dialog.rm_cb.checkState())
         accepted = result == QDialog.Accepted
@@ -255,7 +256,11 @@ class KernelConnectionDialog(QDialog):
                 falsy_to_none(dialog.pw.text()), # ssh password
                 accepted)                        # ok
         else:
-            return (dialog.cf.text(), None, None, None, accepted)
+            path = dialog.cf.text()
+            _dir, filename = osp.dirname(path), osp.basename(path)
+            if _dir == '' and not filename.endswith('.json'):
+                path = osp.join(jupyter_runtime_dir(), 'kernel-'+path+'.json')
+            return (path, None, None, None, accepted)
 
 
 #------------------------------------------------------------------------------
@@ -612,6 +617,11 @@ class IPythonConsole(SpyderPluginWidget):
         if not self.testing:
             self.initialize_plugin()
 
+        # Create temp dir on testing to save kernel errors
+        if self.testing:
+            if not osp.isdir(programs.TEMPDIR):
+                os.mkdir(programs.TEMPDIR)
+
         layout = QVBoxLayout()
         self.tabwidget = Tabs(self, self.menu_actions)
         if hasattr(self.tabwidget, 'setDocumentMode')\
@@ -784,6 +794,7 @@ class IPythonConsole(SpyderPluginWidget):
                          lambda fname, lineno, word, processevents:
                              self.editor.load(fname, lineno, word,
                                               processevents=processevents))
+        self.editor.breakpoints_saved.connect(self.set_spyder_breakpoints)
         self.editor.run_in_current_ipyclient.connect(
                                          self.run_script_in_current_client)
         self.main.workingdirectory.set_current_console_wd.connect(
@@ -1205,6 +1216,11 @@ class IPythonConsole(SpyderPluginWidget):
         self.activateWindow()
         shellwidget._control.setFocus()
 
+    def set_spyder_breakpoints(self):
+        """Set Spyder breakpoints into all clients"""
+        for cl in self.clients:
+            cl.shellwidget.set_spyder_breakpoints()
+
     #------ Public API (for kernels) ------------------------------------------
     def ssh_tunnel(self, *args, **kwargs):
         if os.name == 'nt':
@@ -1326,11 +1342,8 @@ class IPythonConsole(SpyderPluginWidget):
         kernel_manager._kernel_spec = self.create_kernel_spec()
 
         # Save stderr in a file to read it later in case of errors
-        if not self.testing:
-            stderr = codecs.open(stderr_file, 'w', encoding='utf-8')
-            kernel_manager.start_kernel(stderr=stderr)
-        else:
-            kernel_manager.start_kernel()
+        stderr = codecs.open(stderr_file, 'w', encoding='utf-8')
+        kernel_manager.start_kernel(stderr=stderr)
 
         # Kernel client
         kernel_client = kernel_manager.client()
diff --git a/spyder/plugins/tests/test_ipythonconsole.py b/spyder/plugins/tests/test_ipythonconsole.py
index 06c353f..2bad799 100644
--- a/spyder/plugins/tests/test_ipythonconsole.py
+++ b/spyder/plugins/tests/test_ipythonconsole.py
@@ -5,33 +5,136 @@
 #
 
 import os
+import os.path as osp
+import shutil
+import tempfile
 
+from flaky import flaky
 import pytest
-from qtpy.QtCore import Qt
-from pytestqt import qtbot
-from spyder.py3compat import to_text_string
-from spyder.plugins.ipythonconsole import IPythonConsole
+from qtpy.QtCore import Qt, QTimer
+from qtpy.QtWidgets import QApplication
 
+from spyder.plugins.ipythonconsole import (IPythonConsole,
+                                           KernelConnectionDialog)
 
+
+#==============================================================================
+# Constants
+#==============================================================================
+SHELL_TIMEOUT = 20000
+
+
+#==============================================================================
+# Utillity Functions
+#==============================================================================
+def open_client_from_connection_info(connection_info, qtbot):
+    top_level_widgets = QApplication.topLevelWidgets()
+    for w in top_level_widgets:
+        if isinstance(w, KernelConnectionDialog):
+            w.cf.setText(connection_info)
+            qtbot.keyClick(w, Qt.Key_Enter)
+
+
+#==============================================================================
 # Qt Test Fixtures
-#--------------------------------
+#==============================================================================
 @pytest.fixture
-def ipyconsole_bot(qtbot):
+def ipyconsole(request):
     widget = IPythonConsole(None, testing=True)
     widget.create_new_client()
-    qtbot.addWidget(widget)
-    return qtbot, widget
+    def close_widget():
+        widget.close()
+    request.addfinalizer(close_widget)
+    widget.show()
+    return widget
 
 
+#==============================================================================
 # Tests
-#-------------------------------
- at pytest.mark.skipif(os.name == 'nt', reason="It's timing out on Windows")
-def test_sys_argv_clear(ipyconsole_bot):
-    qtbot, ipyconsole = ipyconsole_bot
+#==============================================================================
+ at flaky(max_runs=3)
+ at pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_load_kernel_file_from_id(ipyconsole, qtbot):
+    """
+    Test that a new client is created using its id
+    """
+    shell = ipyconsole.get_current_shellwidget()
+    client = ipyconsole.get_current_client()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    connection_file = osp.basename(client.connection_file)
+    id_ = connection_file.split('kernel-')[-1].split('.json')[0]
+
+    QTimer.singleShot(2000, lambda: open_client_from_connection_info(
+                                        id_, qtbot))
+    ipyconsole.create_client_for_kernel()
+    qtbot.wait(1000)
+
+    new_client = ipyconsole.get_clients()[1]
+    assert new_client.name == '1/B'
+
+
+ at flaky(max_runs=10)
+ at pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_load_kernel_file_from_location(ipyconsole, qtbot):
+    """
+    Test that a new client is created using a connection file
+    placed in a different location from jupyter_runtime_dir
+    """
     shell = ipyconsole.get_current_shellwidget()
     client = ipyconsole.get_current_client()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    connection_file = osp.join(tempfile.gettempdir(),
+                               osp.basename(client.connection_file))
+    shutil.copy2(client.connection_file, connection_file)
+
+    QTimer.singleShot(2000, lambda: open_client_from_connection_info(
+                                        connection_file,
+                                        qtbot))
+    ipyconsole.create_client_for_kernel()
+    qtbot.wait(1000)
+
+    assert len(ipyconsole.get_clients()) == 2
+
+
+ at flaky(max_runs=10)
+ at pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_load_kernel_file(ipyconsole, qtbot):
+    """
+    Test that a new client is created using the connection file
+    of an existing client
+    """
+    shell = ipyconsole.get_current_shellwidget()
+    client = ipyconsole.get_current_client()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
+
+    QTimer.singleShot(2000, lambda: open_client_from_connection_info(
+                                        client.connection_file,
+                                        qtbot))
+    ipyconsole.create_client_for_kernel()
+    qtbot.wait(1000)
+
+    new_client = ipyconsole.get_clients()[1]
+    new_shell = new_client.shellwidget
+    new_shell.execute('a = 10')
+    qtbot.wait(500)
+
+    assert new_client.name == '1/B'
+    assert shell.get_value('a') == new_shell.get_value('a')
+
+
+ at flaky(max_runs=10)
+ at pytest.mark.skipif(os.name == 'nt', reason="It times out on Windows")
+def test_sys_argv_clear(ipyconsole, qtbot):
+    """Test that sys.argv is cleared up correctly"""
+    shell = ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
 
-    qtbot.waitUntil(lambda: shell._prompt_html is not None, timeout=6000)
     shell.execute('import sys; A = sys.argv')
     argv = shell.get_value("A")
     assert argv == ['']
+
+
+if __name__ == "__main__":
+    pytest.main()
diff --git a/spyder/utils/fixtures.py b/spyder/utils/fixtures.py
index 63a0c86..e236939 100644
--- a/spyder/utils/fixtures.py
+++ b/spyder/utils/fixtures.py
@@ -11,11 +11,18 @@ Testing utilities to be used with pytest.
 # Standard library imports
 import shutil
 import tempfile
+try:
+    from unittest.mock import Mock
+except ImportError:
+    from mock import Mock # Python 2
 
 # Third party imports
 import pytest
 
 # Local imports
+# Local imports
+from spyder.widgets.editor import EditorStack
+from spyder.widgets.findreplace import FindReplace
 from spyder.config.user import UserConfig
 from spyder.config.main import CONF_VERSION, DEFAULTS
 
@@ -41,3 +48,22 @@ def tmpconfig(request):
 
     request.addfinalizer(fin)
     return CONF
+
+ at pytest.fixture
+def setup_editor(qtbot):
+    """
+    Set up EditorStack with CodeEditor containing some Python code.
+    The cursor is at the empty line below the code.
+    Returns tuple with EditorStack and CodeEditor.
+    """
+    text = ('a = 1\n'
+            'print(a)\n'
+            '\n'
+            'x = 2')  # a newline is added at end
+    editorStack = EditorStack(None, [])
+    editorStack.set_introspector(Mock())
+    editorStack.set_find_widget(FindReplace(editorStack))
+    editorStack.set_io_actions(Mock(), Mock(), Mock(), Mock())
+    finfo = editorStack.new('foo.py', 'utf-8', text)
+    qtbot.addWidget(editorStack)
+    return editorStack, finfo.editor
diff --git a/spyder/utils/introspection/docstrings.py b/spyder/utils/introspection/docstrings.py
deleted file mode 100644
index e107660..0000000
--- a/spyder/utils/introspection/docstrings.py
+++ /dev/null
@@ -1,285 +0,0 @@
-"""
-Docstrings are another source of information for functions and classes.
-:mod:`jedi.evaluate.dynamic` tries to find all executions of functions, while
-the docstring parsing is much easier. There are two different types of
-docstrings that |jedi| understands:
-
-- `Sphinx <http://sphinx-doc.org/markup/desc.html#info-field-lists>`_
-- `Epydoc <http://epydoc.sourceforge.net/manual-fields.html>`_
-
-For example, the sphinx annotation ``:type foo: str`` clearly states that the
-type of ``foo`` is ``str``.
-
-As an addition to parameter searching, this module also provides return
-annotations.
-"""
-
-from ast import literal_eval
-import re
-from itertools import chain
-from textwrap import dedent
-from jedi import debug
-from jedi.evaluate.cache import memoize_default
-from jedi.parser import Parser, load_grammar
-from jedi.parser.tree import Class
-from jedi.common import indent_block
-from jedi.evaluate.iterable import Array, FakeSequence, AlreadyEvaluated
-
-
-DOCSTRING_PARAM_PATTERNS = [
-    r'\s*:type\s+%s:\s*([^\n]+)',  # Sphinx
-    r'\s*:param\s+(\w+)\s+%s:[^\n]+',  # Sphinx param with type
-    r'\s*@type\s+%s:\s*([^\n]+)',  # Epydoc
-]
-
-DOCSTRING_RETURN_PATTERNS = [
-    re.compile(r'\s*:rtype:\s*([^\n]+)', re.M),  # Sphinx
-    re.compile(r'\s*@rtype:\s*([^\n]+)', re.M),  # Epydoc
-]
-
-REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`')
-
-
-try:
-    from numpydoc.docscrape import NumpyDocString
-except ImportError:
-    def _search_param_in_numpydocstr(docstr, param_str):
-        return []
-
-    def _search_return_in_numpydocstr(docstr):
-        return []
-else:
-    def _search_param_in_numpydocstr(docstr, param_str):
-        """Search `docstr` (in numpydoc format) for type(-s) of `param_str`."""
-        params = NumpyDocString(docstr)._parsed_data['Parameters']
-        for p_name, p_type, p_descr in params:
-            if p_name == param_str:
-                m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type)
-                if m:
-                    p_type = m.group(1)
-                return _expand_typestr(p_type)
-        return []
-
-    def _search_return_in_numpydocstr(docstr):
-        r"""
-        Search `docstr` (in numpydoc format) for type(-s) of `param_str`.
-        """
-        doc = NumpyDocString(docstr)
-        returns = doc._parsed_data['Returns']
-        returns += doc._parsed_data['Yields']
-        found = []
-        for p_name, p_type, p_descr in returns:
-            if not p_type:
-                p_type = p_name
-                p_name = ''
-
-            m = re.match('([^,]+(,[^,]+)*?)$', p_type)
-            if m:
-                p_type = m.group(1)
-            found.extend(_expand_typestr(p_type))
-        return found
-
-
-def _expand_typestr(p_type):
-    """
-    Attempts to interpret the possible types
-    """
-    # Check if alternative types are specified
-    if re.search('\\bor\\b', p_type):
-        types = [t.strip() for t in p_type.split('or')]
-    # Check if type has a set of valid literal values
-    elif p_type.startswith('{'):
-        # python2 does not support literal set evals
-        # workaround this by using lists instead
-        p_type = p_type.replace('{', '[').replace('}', ']')
-        types = set(type(x).__name__ for x in literal_eval(p_type))
-        types = list(types)
-    # Otherwise just return the typestr wrapped in a list
-    else:
-        types = [p_type]
-    return types
-
-
-def _search_param_in_docstr(docstr, param_str):
-    """
-    Search `docstr` for type(-s) of `param_str`.
-
-    >>> _search_param_in_docstr(':type param: int', 'param')
-    ['int']
-    >>> _search_param_in_docstr('@type param: int', 'param')
-    ['int']
-    >>> _search_param_in_docstr(
-    ...   ':type param: :class:`threading.Thread`', 'param')
-    ['threading.Thread']
-    >>> bool(_search_param_in_docstr('no document', 'param'))
-    False
-    >>> _search_param_in_docstr(':param int param: some description', 'param')
-    ['int']
-    """
-    # look at #40 to see definitions of those params
-
-    # Check for Sphinx/Epydoc params
-    patterns = [re.compile(p % re.escape(param_str))
-                for p in DOCSTRING_PARAM_PATTERNS]
-
-    found = None
-    for pattern in patterns:
-        match = pattern.search(docstr)
-        if match:
-            found = [_strip_rst_role(match.group(1))]
-            break
-    if found is not None:
-        return found
-
-    # Check for numpy style params
-    found = _search_param_in_numpydocstr(docstr, param_str)
-    if found is not None:
-        return found
-
-    return []
-
-
-def _strip_rst_role(type_str):
-    """
-    Strip off the part looks like a ReST role in `type_str`.
-
-    >>> _strip_rst_role(':class:`ClassName`')  # strip off :class:
-    'ClassName'
-    >>> _strip_rst_role(':py:obj:`module.Object`')  # works with domain
-    'module.Object'
-    >>> _strip_rst_role('ClassName')  # do nothing when not ReST role
-    'ClassName'
-
-    See also:
-    http://sphinx-doc.org/domains.html#cross-referencing-python-objects
-    """
-    match = REST_ROLE_PATTERN.match(type_str)
-    if match:
-        return match.group(1)
-    else:
-        return type_str
-
-
-def _evaluate_for_statement_string(evaluator, string, module):
-    if string is None:
-        return []
-
-    code = dedent("""
-    def pseudo_docstring_stuff():
-        # Create a pseudo function for docstring statements.
-    %s
-    """)
-
-    for element in re.findall('((?:\w+\.)*\w+)\.', string):
-        # Try to import module part in dotted name.
-        # (e.g., 'threading' in 'threading.Thread').
-        string = 'import %s\n' % element + string
-
-    # Take the default grammar here, if we load the Python 2.7 grammar here, it
-    # will be impossible to use `...` (Ellipsis) as a token. Docstring types
-    # don't need to conform with the current grammar.
-    p = Parser(load_grammar(), code % indent_block(string))
-    try:
-        pseudo_cls = p.module.subscopes[0]
-        # First pick suite, then simple_stmt (-2 for DEDENT) and then the node,
-        # which is also not the last item, because there's a newline.
-        stmt = pseudo_cls.children[-1].children[-2].children[-2]
-    except (AttributeError, IndexError):
-        type_list = []
-    else:
-        # Use the module of the param.
-        # TODO this module is not the module of the param in case of a function
-        # call. In that case it's the module of the function call.
-        # stuffed with content from a function call.
-        pseudo_cls.parent = module
-        type_list = _execute_types_in_stmt(evaluator, stmt)
-    return type_list
-
-
-def _execute_types_in_stmt(evaluator, stmt):
-    """
-    Executing all types or general elements that we find in a statement. This
-    doesn't include tuple, list and dict literals, because the stuff they
-    contain is executed. (Used as type information).
-    """
-    definitions = evaluator.eval_element(stmt)
-    types_list = [_execute_array_values(evaluator, d) for d in definitions]
-    type_list = list(chain.from_iterable(types_list))
-    return type_list
-
-
-def _execute_array_values(evaluator, array):
-    """
-    Tuples indicate that there's not just one return value, but the listed
-    ones.  `(str, int)` means that it returns a tuple with both types.
-    """
-    if isinstance(array, Array):
-        values = []
-        for types in array.py__iter__():
-            objects = set(chain.from_iterable(_execute_array_values(evaluator, typ) for typ in types))
-            values.append(AlreadyEvaluated(objects))
-        return [FakeSequence(evaluator, values, array.type)]
-    else:
-        return evaluator.execute(array)
-
-
- at memoize_default(None, evaluator_is_first_arg=True)
-def follow_param(evaluator, param):
-    """
-    Determines a set of potential types for `param` using docstring hints
-
-    :type evaluator: jedi.evaluate.Evaluator
-    :type param: jedi.parser.tree.Param
-
-    :rtype: list
-    """
-    def eval_docstring(docstr):
-        param_str = str(param.name)
-        return set(
-            [p for string in _search_param_in_docstr(docstr, param_str)
-                for p in _evaluate_for_statement_string(evaluator, string, module)]
-        )
-    func = param.parent_function
-    module = param.get_parent_until()
-
-    docstr = func.raw_doc
-    types = eval_docstring(docstr)
-    if func.name.value == '__init__':
-        cls = func.get_parent_until(Class)
-        if cls.type == 'classdef':
-            types |= eval_docstring(cls.raw_doc)
-
-    return types
-
-
- at memoize_default(None, evaluator_is_first_arg=True)
-def find_return_types(evaluator, func):
-    """
-    Determines a set of potential return types for `func` using docstring hints
-
-    :type evaluator: jedi.evaluate.Evaluator
-    :type param: jedi.parser.tree.Param
-
-    :rtype: list
-    """
-    def search_return_in_docstr(docstr):
-        # Check for Sphinx/Epydoc return hint
-        for p in DOCSTRING_RETURN_PATTERNS:
-            match = p.search(docstr)
-            if match:
-                return [_strip_rst_role(match.group(1))]
-        found = []
-        if not found:
-            # Check for numpy style return hint
-            found = _search_return_in_numpydocstr(docstr)
-        return found
-
-    docstr = func.raw_doc
-    module = func.get_parent_until()
-    types = []
-    for type_str in search_return_in_docstr(docstr):
-        type_ = _evaluate_for_statement_string(evaluator, type_str, module)
-        types.extend(type_)
-    debug.dbg('DOC!!!!!!!!!!!!!! wow types?: %s in %s',types, func)
-    return types
-
diff --git a/spyder/utils/introspection/jedi_patch.py b/spyder/utils/introspection/jedi_patch.py
index 347f96c..1790be1 100644
--- a/spyder/utils/introspection/jedi_patch.py
+++ b/spyder/utils/introspection/jedi_patch.py
@@ -28,8 +28,11 @@ def apply():
         raise ImportError("jedi %s can't be patched" % jedi.__version__)
 
     # [1] Adding numpydoc type returns to docstrings
-    from spyder.utils.introspection import docstrings
-    jedi.evaluate.representation.docstrings = docstrings
+    from spyder.utils.introspection import numpy_docstr
+    jedi.evaluate.representation.docstrings._search_param_in_numpydocstr = \
+        numpy_docstr._search_param_in_numpydocstr
+    jedi.evaluate.representation.docstrings.find_return_types = \
+        numpy_docstr.find_return_types
 
     # [2] Adding type returns for compiled objects in jedi
     # Patching jedi.evaluate.compiled.CompiledObject...
@@ -42,8 +45,8 @@ def apply():
             if self.type != 'funcdef':
                 return
             # patching docstrings here
-            from spyder.utils.introspection import docstrings
-            types = docstrings.find_return_types(evaluator, self)
+            from spyder.utils.introspection import numpy_docstr
+            types = numpy_docstr.find_return_types(evaluator, self)
             if types:
                 for result in types:
                     debug.dbg('docstrings type return: %s in %s', result, self)
@@ -111,7 +114,7 @@ def apply():
     # [4] Fixing introspection for matplotlib Axes objects
     # Patching jedi.evaluate.precedence...
     from jedi.evaluate.representation import (
-        tree, InstanceName, Instance, compiled, FunctionExecution, InstanceElement)
+        InstanceName, Instance, compiled, FunctionExecution, InstanceElement)
 
     def get_instance_el(evaluator, instance, var, is_class_var=False):
         """
diff --git a/spyder/utils/introspection/manager.py b/spyder/utils/introspection/manager.py
index edce2da..eb5df24 100644
--- a/spyder/utils/introspection/manager.py
+++ b/spyder/utils/introspection/manager.py
@@ -33,7 +33,7 @@ dependencies.add('rope',
                  _("Editor's code completion, go-to-definition and help"),
                  required_version=ROPE_REQVER)
 
-JEDI_REQVER = '>=0.8.1'
+JEDI_REQVER = '=0.9.0'
 dependencies.add('jedi',
                  _("Editor's code completion, go-to-definition and help"),
                  required_version=JEDI_REQVER)
diff --git a/spyder/utils/introspection/numpy_docstr.py b/spyder/utils/introspection/numpy_docstr.py
new file mode 100644
index 0000000..c3ff0e8
--- /dev/null
+++ b/spyder/utils/introspection/numpy_docstr.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+# Contents in this file are taken from
+#
+# https://github.com/davidhalter/jedi/pull/796
+#
+# to patch Jedi 0.9.0 (it probably doesn't work with
+# higher versions)
+
+
+from ast import literal_eval
+import re
+
+from jedi._compatibility import is_py3
+from jedi.evaluate.cache import memoize_default
+from jedi.evaluate.docstrings import (_evaluate_for_statement_string,
+                                      _strip_rst_role,
+                                      DOCSTRING_RETURN_PATTERNS)
+from numpydoc.docscrape import NumpyDocString
+
+
+def _expand_typestr(p_type):
+    """
+    Attempts to interpret the possible types
+    """
+    # Check if alternative types are specified
+    if re.search('\\bor\\b', p_type):
+        types = [t.strip() for t in p_type.split('or')]
+    # Check if type has a set of valid literal values
+    elif p_type.startswith('{'):
+        if not is_py3:
+            # python2 does not support literal set evals
+            # workaround this by using lists instead
+            p_type = p_type.replace('{', '[').replace('}', ']')
+        types = set(type(x).__name__ for x in literal_eval(p_type))
+        types = list(types)
+    # Otherwise just return the typestr wrapped in a list
+    else:
+        types = [p_type]
+    return types
+
+
+def _search_param_in_numpydocstr(docstr, param_str):
+    r"""
+    Search `docstr` (in numpydoc format) for type(-s) of `param_str`.
+    >>> from jedi.evaluate.docstrings import *  # NOQA
+    >>> from jedi.evaluate.docstrings import _search_param_in_numpydocstr
+    >>> docstr = (
+    ...    'Parameters\n'
+    ...    '----------\n'
+    ...    'x : ndarray\n'
+    ...    'y : int or str or list\n'
+    ...    'z : {"foo", "bar", 100500}, optional\n'
+    ... )
+    >>> _search_param_in_numpydocstr(docstr, 'x')
+    ['ndarray']
+    >>> sorted(_search_param_in_numpydocstr(docstr, 'y'))
+    ['int', 'list', 'str']
+    >>> sorted(_search_param_in_numpydocstr(docstr, 'z'))
+    ['int', 'str']
+    """
+    params = NumpyDocString(docstr)._parsed_data['Parameters']
+    for p_name, p_type, p_descr in params:
+        if p_name == param_str:
+            m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type)
+            if m:
+                p_type = m.group(1)
+            return _expand_typestr(p_type)
+    return []
+
+
+def _search_return_in_numpydocstr(docstr):
+    r"""
+    Search `docstr` (in numpydoc format) for type(-s) of `param_str`.
+    >>> from jedi.evaluate.docstrings import *  # NOQA
+    >>> from jedi.evaluate.docstrings import _search_return_in_numpydocstr
+    >>> from jedi.evaluate.docstrings import _expand_typestr
+    >>> docstr = (
+    ...    'Returns\n'
+    ...    '----------\n'
+    ...    'int\n'
+    ...    '    can return an anoymous integer\n'
+    ...    'out : ndarray\n'
+    ...    '    can return a named value\n'
+    ... )
+    >>> _search_return_in_numpydocstr(docstr)
+    ['int', 'ndarray']
+    """
+    doc = NumpyDocString(docstr)
+    returns = doc._parsed_data['Returns']
+    returns += doc._parsed_data['Yields']
+    found = []
+    for p_name, p_type, p_descr in returns:
+        if not p_type:
+            p_type = p_name
+            p_name = ''
+
+        m = re.match('([^,]+(,[^,]+)*?)$', p_type)
+        if m:
+            p_type = m.group(1)
+        found.extend(_expand_typestr(p_type))
+    return found
+
+
+ at memoize_default(None, evaluator_is_first_arg=True)
+def find_return_types(evaluator, func):
+    """
+    Determines a set of potential return types for `func` using docstring hints
+    :type evaluator: jedi.evaluate.Evaluator
+    :type param: jedi.parser.tree.Param
+    :rtype: list
+    >>> from jedi.evaluate.docstrings import *  # NOQA
+    >>> from jedi.evaluate.docstrings import _search_param_in_docstr
+    >>> from jedi.evaluate.docstrings import _evaluate_for_statement_string
+    >>> from jedi.evaluate.docstrings import _search_return_in_gooogledocstr
+    >>> from jedi.evaluate.docstrings import _search_return_in_numpydocstr
+    >>> from jedi._compatibility import builtins
+    >>> source = open(jedi.evaluate.docstrings.__file__.replace('.pyc', '.py'), 'r').read()
+    >>> script = jedi.Script(source)
+    >>> evaluator = script._evaluator
+    >>> func = script._get_module().names_dict['find_return_types'][0].parent
+    >>> types = find_return_types(evaluator, func)
+    >>> print('types = %r' % (types,))
+    >>> assert len(types) == 1
+    >>> assert types[0].base.obj is builtins.list
+    """
+    def search_return_in_docstr(docstr):
+        # Check for Sphinx/Epydoc return hint
+        for p in DOCSTRING_RETURN_PATTERNS:
+            match = p.search(docstr)
+            if match:
+                return [_strip_rst_role(match.group(1))]
+        found = []
+
+        if not found:
+            # Check for numpy style return hint
+            found = _search_return_in_numpydocstr(docstr)
+        return found
+
+    docstr = func.raw_doc
+    module = func.get_parent_until()
+    types = []
+    for type_str in search_return_in_docstr(docstr):
+        type_ = _evaluate_for_statement_string(evaluator, type_str, module)
+        types.extend(type_)
+    return types
diff --git a/spyder/utils/introspection/test/test_jedi_plugin.py b/spyder/utils/introspection/test/test_jedi_plugin.py
index 68d8f42..eb8e870 100644
--- a/spyder/utils/introspection/test/test_jedi_plugin.py
+++ b/spyder/utils/introspection/test/test_jedi_plugin.py
@@ -55,7 +55,7 @@ def test_get_path():
     source_code = 'from spyder.utils.introspection.manager import CodeInfo'
     path, line_nr = p.get_definition(CodeInfo('definition', source_code,
                                               len(source_code), __file__))
-    assert 'utils.py' in path and 'introspection' in path
+    assert 'utils' in path and 'introspection' in path
 
 
 def test_get_docstring():
diff --git a/spyder/utils/iofuncs.py b/spyder/utils/iofuncs.py
index d2600e7..9a33da9 100644
--- a/spyder/utils/iofuncs.py
+++ b/spyder/utils/iofuncs.py
@@ -16,6 +16,7 @@ from __future__ import print_function
 import sys
 import os
 import tarfile
+import tempfile
 import os.path as osp
 import shutil
 import warnings
@@ -338,13 +339,15 @@ def load_dictionary(filename):
     """Load dictionary from .spydata file"""
     filename = osp.abspath(filename)
     old_cwd = getcwd()
-    os.chdir(osp.dirname(filename))
+    tmp_folder = tempfile.mkdtemp()
+    os.chdir(tmp_folder)
     data = None
     error_message = None
     try:
         tar = tarfile.open(filename, "r")
         tar.extractall()
-        pickle_filename = osp.splitext(filename)[0]+'.pickle'
+        data_file = osp.basename(filename)
+        pickle_filename = osp.splitext(data_file)[0]+'.pickle'
         try:
             # Old format (Spyder 2.0-2.1 for Python 2)
             with open(pickle_filename, 'U') as fdesc:
@@ -359,7 +362,7 @@ def load_dictionary(filename):
             try:
                 saved_arrays = data.pop('__saved_arrays__')
                 for (name, index), fname in list(saved_arrays.items()):
-                    arr = np.load( osp.join(osp.dirname(filename), fname) )
+                    arr = np.load( osp.join(tmp_folder, fname) )
                     if index is None:
                         data[name] = arr
                     elif isinstance(data[name], dict):
@@ -368,11 +371,13 @@ def load_dictionary(filename):
                         data[name].insert(index, arr)
             except KeyError:
                 pass
-        for fname in [pickle_filename]+[fn for fn in list(saved_arrays.values())]:
-            os.remove(fname)
     except (EOFError, ValueError) as error:
         error_message = to_text_string(error)
     os.chdir(old_cwd)
+    try:
+        shutil.rmtree(tmp_folder)
+    except OSError as error:
+        error_message = to_text_string(error)
     return data, error_message
 
 
diff --git a/spyder/utils/ipython/spyder_kernel.py b/spyder/utils/ipython/spyder_kernel.py
index f4c4711..b390d85 100644
--- a/spyder/utils/ipython/spyder_kernel.py
+++ b/spyder/utils/ipython/spyder_kernel.py
@@ -83,7 +83,7 @@ class SpyderKernel(IPythonKernel):
             return {}
 
     # -- Public API ---------------------------------------------------
-    # For the Variable Explorer
+    # --- For the Variable Explorer
     def get_namespace_view(self):
         """
         Return the namespace view
@@ -329,6 +329,12 @@ class SpyderKernel(IPythonKernel):
         """Register Pdb session to use it later"""
         self._pdb_obj = pdb_obj
 
+    def _set_spyder_breakpoints(self):
+        """Set all Spyder breakpoints in an active pdb session"""
+        if not self._pdb_obj:
+            return
+        self._pdb_obj.set_spyder_breakpoints()
+
     # --- For the Help plugin
     def _eval(self, text):
         """
diff --git a/spyder/widgets/editor.py b/spyder/widgets/editor.py
index dbc30ee..a4fff83 100644
--- a/spyder/widgets/editor.py
+++ b/spyder/widgets/editor.py
@@ -346,9 +346,9 @@ class EditorStack(QWidget):
         fileswitcher_action = create_action(self, _("File switcher..."),
                 icon=ima.icon('filelist'),
                 triggered=self.open_fileswitcher_dlg)
-        symbolfinder_action = create_action(self, 
+        symbolfinder_action = create_action(self,
                 _("Find symbols in file..."),
-                icon=ima.icon('filelist'),
+                icon=ima.icon('symbol_find'),
                 triggered=self.open_symbolfinder_dlg)
         copy_to_cb_action = create_action(self, _("Copy path to clipboard"),
                 icon=ima.icon('editcopy'),
diff --git a/spyder/widgets/findinfiles.py b/spyder/widgets/findinfiles.py
index 1a8cd1e..6785362 100644
--- a/spyder/widgets/findinfiles.py
+++ b/spyder/widgets/findinfiles.py
@@ -397,12 +397,9 @@ class FindOptions(QWidget):
         for widget in [self.python_path, self.hg_manifest, self.custom_dir,
                        self.dir_combo, browse]:
             hlayout3.addWidget(widget)
-            
+
         self.search_text.valid.connect(lambda valid: self.find.emit())
-        self.include_pattern.valid.connect(lambda valid: self.find.emit())
-        self.exclude_pattern.valid.connect(lambda valid: self.find.emit())
-        self.dir_combo.valid.connect(lambda valid: self.find.emit())
-            
+
         vlayout = QVBoxLayout()
         vlayout.setContentsMargins(0, 0, 0, 0)
         vlayout.addLayout(hlayout1)
@@ -411,7 +408,7 @@ class FindOptions(QWidget):
         self.more_widgets = (hlayout2, hlayout3)
         self.toggle_more_options(more_options)
         self.setLayout(vlayout)
-                
+
         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
 
     @Slot(bool)
diff --git a/spyder/widgets/ipythonconsole/debugging.py b/spyder/widgets/ipythonconsole/debugging.py
index 93e4417..8ba4e4c 100644
--- a/spyder/widgets/ipythonconsole/debugging.py
+++ b/spyder/widgets/ipythonconsole/debugging.py
@@ -72,6 +72,12 @@ class DebuggingWidget(RichJupyterWidget):
         # Run post exec commands
         self._post_exec_input(line)
 
+    def set_spyder_breakpoints(self):
+        """Set Spyder breakpoints into a debugging session"""
+        if self._reading:
+            self.kernel_client.input(
+                "!get_ipython().kernel._set_spyder_breakpoints()")
+
     # ---- Private API (defined by us) -------------------------------
     def _post_exec_input(self, line):
         """Commands to be run after writing to stdin"""
diff --git a/spyder/widgets/ipythonconsole/namespacebrowser.py b/spyder/widgets/ipythonconsole/namespacebrowser.py
index f5eec1a..4494101 100644
--- a/spyder/widgets/ipythonconsole/namespacebrowser.py
+++ b/spyder/widgets/ipythonconsole/namespacebrowser.py
@@ -116,7 +116,7 @@ class NamepaceBrowserWidget(RichJupyterWidget):
         wait_loop = QEventLoop()
         self.sig_got_reply.connect(wait_loop.quit)
         self.silent_exec_method(
-                "get_ipython().kernel.load_data('%s', '%s')" % (filename, ext))
+                r"get_ipython().kernel.load_data('%s', '%s')" % (filename, ext))
         wait_loop.exec_()
 
         # Remove loop connection and loop
@@ -129,7 +129,7 @@ class NamepaceBrowserWidget(RichJupyterWidget):
         # Wait until the kernel tries to save the file
         wait_loop = QEventLoop()
         self.sig_got_reply.connect(wait_loop.quit)
-        self.silent_exec_method("get_ipython().kernel.save_namespace('%s')" %
+        self.silent_exec_method(r"get_ipython().kernel.save_namespace('%s')" %
                                 filename)
         wait_loop.exec_()
 
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
index 97b2837..ac492fc 100644
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -477,6 +477,8 @@ class BaseEditMixin(object):
         findflag = QTextDocument.FindFlag()
         if not forward:
             findflag = findflag | QTextDocument.FindBackward
+        if case:
+            findflag = findflag | QTextDocument.FindCaseSensitively
         moves = [QTextCursor.NoMove]
         if forward:
             moves += [QTextCursor.NextWord, QTextCursor.Start]
diff --git a/spyder/widgets/sourcecode/base.py b/spyder/widgets/sourcecode/base.py
index 7b4c35d..93b9812 100644
--- a/spyder/widgets/sourcecode/base.py
+++ b/spyder/widgets/sourcecode/base.py
@@ -629,6 +629,8 @@ class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin):
             self.setTextCursor(cursor)
         text = self.get_selection_as_executable_code()
         self.__restore_selection(start_pos, end_pos)
+        if text is not None:
+            text = text.rstrip()
         return text
 
     def is_cell_separator(self, cursor=None, block=None):
diff --git a/spyder/widgets/sourcecode/codeeditor.py b/spyder/widgets/sourcecode/codeeditor.py
index 846181c..6c6d15d 100644
--- a/spyder/widgets/sourcecode/codeeditor.py
+++ b/spyder/widgets/sourcecode/codeeditor.py
@@ -1985,9 +1985,36 @@ class CodeEditor(TextEditBaseWidget):
                 else:
                     correct_indent -= len(self.indent_chars)
             elif len(re.split(r'\(|\{|\[', prevtext)) > 1:
+
+                # Check if all braces are matching using a stack
+                stack = ['dummy']  # Dummy elemet to avoid index errors
+                deactivate = None
+                for c in prevtext:
+                    if deactivate is not None:
+                        if c == deactivate:
+                            deactivate = None
+                    elif c in ["'", '"']:
+                        deactivate = c
+                    elif c in ['(', '[','{']:
+                        stack.append(c)
+                    elif c == ')' and stack[-1] == '(':
+                        stack.pop()
+                    elif c == ']' and stack[-1] == '[':
+                        stack.pop()
+                    elif c == '}' and stack[-1] == '{':
+                        stack.pop()
+
+                if len(stack) == 1:  # all braces matching
+                    pass
+
                 # Hanging indent
                 # find out if the last one is (, {, or []})
-                if re.search(r'[\(|\{|\[]\s*$', prevtext) is not None:
+                # only if prevtext is long that the hanging indentation
+                elif (re.search(r'[\(|\{|\[]\s*$', prevtext) is not None and
+                      ((self.indent_chars == '\t' and
+                        self.tab_stop_width_spaces * 2 < len(prevtext)) or
+                       (self.indent_chars.startswith(' ') and
+                        len(self.indent_chars) * 2 < len(prevtext)))):
                     if self.indent_chars == '\t':
                         correct_indent += self.tab_stop_width_spaces * 2
                     else:
@@ -2037,6 +2064,9 @@ class CodeEditor(TextEditBaseWidget):
         if correct_indent >= 0:
             cursor = self.textCursor()
             cursor.movePosition(QTextCursor.StartOfBlock)
+            if self.indent_chars == '\t':
+                indent = indent // self.tab_stop_width_spaces \
+                + correct_indent % self.tab_stop_width_spaces
             cursor.setPosition(cursor.position()+indent, QTextCursor.KeepAnchor)
             cursor.removeSelectedText()
             if self.indent_chars == '\t':
@@ -2365,9 +2395,11 @@ class CodeEditor(TextEditBaseWidget):
         self.setTextCursor(cursor)
 
     #------Autoinsertion of quotes/colons
-    def __get_current_color(self):
+    def __get_current_color(self, cursor=None):
         """Get the syntax highlighting color for the current cursor position"""
-        cursor = self.textCursor()
+        if cursor is None:
+            cursor = self.textCursor()
+
         block = cursor.block()
         pos = cursor.position() - block.position()  # relative pos within block
         layout = block.layout()
@@ -2382,15 +2414,21 @@ class CodeEditor(TextEditBaseWidget):
                 for fmt in block_formats:
                     if (pos >= fmt.start) and (pos < fmt.start + fmt.length):
                         current_format = fmt.format
+                if current_format is None:
+                    return None
             color = current_format.foreground().color().name()
             return color
         else:
             return None
 
-    def in_comment_or_string(self):
+    def in_comment_or_string(self, cursor=None):
         """Is the cursor inside or next to a comment or string?"""
         if self.highlighter:
-            current_color = self.__get_current_color()
+            if cursor is None:
+                current_color = self.__get_current_color()
+            else:
+                current_color = self.__get_current_color(cursor=cursor)
+
             comment_color = self.highlighter.get_color_name('comment')
             string_color = self.highlighter.get_color_name('string')
             if (current_color == comment_color) or (current_color == string_color):
@@ -2693,7 +2731,20 @@ class CodeEditor(TextEditBaseWidget):
                    and self.codecompletion_enter:
                     self.select_completion_list()
                 else:
-                    cmt_or_str = self.in_comment_or_string()
+                    # Check if we're in a comment or a string at the
+                    # current position
+                    cmt_or_str_cursor = self.in_comment_or_string()
+
+                    # Check if the line start with a comment or string
+                    cursor = self.textCursor()
+                    cursor.setPosition(cursor.block().position(),
+                                       QTextCursor.KeepAnchor)
+                    cmt_or_str_line_begin = self.in_comment_or_string(
+                                                cursor=cursor)
+
+                    # Check if we are in a comment or a string
+                    cmt_or_str = cmt_or_str_cursor and cmt_or_str_line_begin
+
                     self.textCursor().beginEditBlock()
                     TextEditBaseWidget.keyPressEvent(self, event)
                     self.fix_indent(comment_or_string=cmt_or_str)
diff --git a/spyder/widgets/variableexplorer/collectionseditor.py b/spyder/widgets/variableexplorer/collectionseditor.py
index 169b2e0..0a084db 100644
--- a/spyder/widgets/variableexplorer/collectionseditor.py
+++ b/spyder/widgets/variableexplorer/collectionseditor.py
@@ -1315,11 +1315,11 @@ class CollectionsEditor(QDialog):
         buttons = QDialogButtonBox.Ok
         if not readonly:
             buttons = buttons | QDialogButtonBox.Cancel
-        bbox = QDialogButtonBox(buttons)
-        bbox.accepted.connect(self.accept)
+        self.bbox = QDialogButtonBox(buttons)
+        self.bbox.accepted.connect(self.accept)
         if not readonly:
-            bbox.rejected.connect(self.reject)
-        layout.addWidget(bbox)
+            self.bbox.rejected.connect(self.reject)
+        layout.addWidget(self.bbox)
 
         constant = 121
         row_height = 30
diff --git a/spyder/widgets/variableexplorer/dataframeeditor.py b/spyder/widgets/variableexplorer/dataframeeditor.py
index e762636..fd1edd9 100644
--- a/spyder/widgets/variableexplorer/dataframeeditor.py
+++ b/spyder/widgets/variableexplorer/dataframeeditor.py
@@ -43,6 +43,9 @@ COMPLEX_NUMBER_TYPES = (complex, np.complex64, np.complex128)
 # Used to convert bool intrance to false since bool('False') will return True
 _bool_false = ['false', '0']
 
+# Default format for data frames with floats
+DEFAULT_FORMAT = '%.3g'
+
 # Limit at which dataframe is considered so large that it is loaded on demand
 LARGE_SIZE = 5e5
 LARGE_NROWS = 1e5
@@ -83,7 +86,7 @@ class DataFrameModel(QAbstractTableModel):
     ROWS_TO_LOAD = 500
     COLS_TO_LOAD = 40
     
-    def __init__(self, dataFrame, format="%.3g", parent=None):
+    def __init__(self, dataFrame, format=DEFAULT_FORMAT, parent=None):
         QAbstractTableModel.__init__(self)
         self.dialog = parent
         self.df = dataFrame
@@ -274,7 +277,12 @@ class DataFrameModel(QAbstractTableModel):
             else:
                 value = self.get_value(row, column-1)
                 if isinstance(value, float):
-                    return to_qvariant(self._format % value)
+                    try:
+                        return to_qvariant(self._format % value)
+                    except (ValueError, TypeError):
+                        # may happen if format = '%d' and value = NaN;
+                        # see issue 4139
+                        return to_qvariant(DEFAULT_FORMAT % value)
                 else:
                     try:
                         return to_qvariant(to_text_string(value))

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/spyder.git



More information about the debian-science-commits mailing list