[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